diff --git a/.csscomb.json b/.csscomb.json
index 741cc1488b5567ceda1b6f650eb2561429acc9d4..aa6a17f751790ca6b5d89d9ab2b6dc0a0a3ef0e3 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -6,7 +6,7 @@
   "always-semicolon": true,
   "color-case": "lower",
   "block-indent": "  ",
-  "color-shorthand": true,
+  "color-shorthand": false,
   "element-case": "lower",
   "space-before-colon": "",
   "space-after-colon": " ",
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000000000000000000000000000000000..d9c2233c9d784085598b00d48593355adf9e2b56
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,5 @@
+/coverage-javascript/
+/public/
+/tmp/
+/vendor/
+/builds/
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..fd26215b84399973f3316d440a262b7406082c70
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,29 @@
+{
+  "extends": "airbnb",
+  "plugins": [
+    "filenames"
+  ],
+  "rules": {
+    "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
+  },
+  "globals": {
+    "$": false,
+    "_": false,
+    "beforeEach": false,
+    "d3": false,
+    "define": false,
+    "describe": false,
+    "document": false,
+    "expect": false,
+    "fixture": false,
+    "gl": false,
+    "it": false,
+    "jQuery": false,
+    "Mousetrap": false,
+    "spyOn": false,
+    "spyOnEvent": false,
+    "Turbolinks": false,
+    "window": false
+  }
+}
+
diff --git a/.flayignore b/.flayignore
index 9c9875d4f9ef387c191ce57d128a7c2af1abd8b3..44df2ba237170b7f51eb1efad7067a584c520664 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1 +1,3 @@
 *.erb
+lib/gitlab/sanitizers/svg/whitelist.rb
+lib/gitlab/diff/position_tracer.rb
diff --git a/.gitattributes b/.gitattributes
index 17cbaa5eef5e0560c1036e745f1ebf62c078fa64..ab791a4cd6c3a01c1442ccd12ae0c1ccb5123096 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
-CHANGELOG merge=union
+CHANGELOG.md merge=union
 *.js.es6 gitlab-language=javascript
diff --git a/.gitignore b/.gitignore
index 1bf9a47aef6de8a156e6ffb0e34c87de23efc445..6a1002621f4800fc5eb9acefe2c50b75bc77d521 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@
 /doc/code/*
 /dump.rdb
 /log/*.log*
+/node_modules/
 /nohup.out
 /public/assets/
 /public/uploads.*
@@ -48,3 +49,4 @@
 /vendor/bundle/*
 /builds/*
 /shared/*
+/.gitlab_workhorse_secret
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be5614520a58d9b4afda6df81d064107d2e184b8..34348247e91c696b127f2f36a7cc711bb3109eda 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,9 +1,8 @@
-image: "ruby:2.3.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
 
 cache:
   key: "ruby-231"
   paths:
-  - vendor/apt
   - vendor/ruby
 
 variables:
@@ -12,7 +11,7 @@ variables:
   RSPEC_RETRY_RETRY_COUNT: "3"
   RAILS_ENV: "test"
   SIMPLECOV: "true"
-  USE_DB: "true"
+  SETUP_DB: "true"
   USE_BUNDLE_INSTALL: "true"
   GIT_DEPTH: "20"
   PHANTOMJS_VERSION: "2.1.1"
@@ -23,7 +22,7 @@ before_script:
   - bundle --version
   - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
   - retry gem install knapsack
-  - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
+  - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
 
 stages:
 - prepare
@@ -35,7 +34,7 @@ stages:
 .knapsack-state: &knapsack-state
   services: []
   variables:
-    USE_DB: "false"
+    SETUP_DB: "false"
     USE_BUNDLE_INSTALL: "false"
   cache:
     key: "knapsack"
@@ -82,7 +81,7 @@ update-knapsack:
     - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
     - export KNAPSACK_GENERATE_REPORT=true
     - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
-    - knapsack rspec
+    - knapsack rspec "--color --format documentation"
   artifacts:
     expire_in: 31d
     paths:
@@ -100,7 +99,7 @@ update-knapsack:
     - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
     - export KNAPSACK_GENERATE_REPORT=true
     - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
-    - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+    - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
   artifacts:
     expire_in: 31d
     paths:
@@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack
 
 # Execute all testing suites against Ruby 2.1
 .ruby-21: &ruby-21
-  image: "ruby:2.1"
+  image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
   <<: *use-db
   only:
     - master
   cache:
     key: "ruby21"
     paths:
-      - vendor/apt
       - vendor/ruby
 
 .rspec-knapsack-ruby21: &rspec-knapsack-ruby21
@@ -196,7 +194,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
 .ruby-static-analysis: &ruby-static-analysis
   variables:
     SIMPLECOV: "false"
-    USE_DB: "false"
+    SETUP_DB: "false"
     USE_BUNDLE_INSTALL: "true"
 
 .exec: &exec
@@ -206,12 +204,33 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
     - bundle exec $CI_BUILD_NAME
 
 rubocop: *exec
+rake haml_lint: *exec
 rake scss_lint: *exec
 rake brakeman: *exec
-rake flog: *exec
 rake flay: *exec
 license_finder: *exec
 rake downtime_check: *exec
+rake ee_compat_check:
+  <<: *exec
+  only:
+    - branches@gitlab-org/gitlab-ce
+    - branches@gitlab/gitlabhq
+  except:
+    - master
+    - tags
+    - /^[\d-]+-stable(-ee)?$/
+  allow_failure: yes
+  cache:
+    key: "ruby231-ee_compat_check_repo"
+    paths:
+      - ee_compat_check/repo/
+      - vendor/ruby
+  artifacts:
+    name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+    when: on_failure
+    expire_in: 10d
+    paths:
+      - ee_compat_check/patches/*.patch
 
 rake db:migrate:reset:
   stage: test
@@ -219,6 +238,23 @@ rake db:migrate:reset:
   script:
     - rake db:migrate:reset
 
+rake db:seed_fu:
+  stage: test
+  <<: *use-db
+  variables:
+    SIZE: "1"
+    SETUP_DB: "false"
+    RAILS_ENV: "development"
+  script:
+    - git clone https://gitlab.com/gitlab-org/gitlab-test.git
+       /home/git/repositories/gitlab-org/gitlab-test.git
+    - bundle exec rake db:setup db:seed_fu
+  artifacts:
+    when: on_failure
+    expire_in: 1d
+    paths:
+      - log/development.log
+
 teaspoon:
   stage: test
   <<: *use-db
@@ -226,7 +262,7 @@ teaspoon:
     - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
     - apt-get install --assume-yes nodejs
     - npm install --global istanbul
-    - teaspoon
+    - rake teaspoon
   artifacts:
     name: coverage-javascript
     expire_in: 31d
@@ -240,6 +276,12 @@ lint-doc:
   script:
     - scripts/lint-doc.sh
 
+bundler:check:
+ stage: test
+ <<: *ruby-static-analysis
+ script:
+   - bundle check
+
 bundler:audit:
   stage: test
   <<: *ruby-static-analysis
@@ -248,11 +290,30 @@ bundler:audit:
   script:
     - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
 
+migration paths:
+  stage: test
+  <<: *use-db
+  variables:
+    SETUP_DB: "false"
+  only:
+    - master@gitlab-org/gitlab-ce
+  script:
+    - git checkout HEAD .
+    - git fetch --tags
+    - git checkout v8.5.9
+    - cp config/resque.yml.example config/resque.yml
+    - sed -i 's/localhost/redis/g' config/resque.yml
+    - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3
+    - rake db:drop db:create db:schema:load db:seed_fu
+    - git checkout $CI_BUILD_REF
+    - source scripts/prepare_build.sh
+    - rake db:migrate
+
 coverage:
   stage: post-test
   services: []
   variables:
-    USE_DB: "false"
+    SETUP_DB: "false"
     USE_BUNDLE_INSTALL: "true"
   script:
     - bundle exec scripts/merge-simplecov
@@ -263,13 +324,36 @@ coverage:
     - coverage/index.html
     - coverage/assets/
 
+lint-javascript:
+  stage: test
+  image: "node:latest"
+  before_script:
+    - npm install
+  script:
+    - npm run eslint
+
+# Trigger docs build
+# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
+trigger_docs:
+  stage: post-test
+  image: "alpine"
+  before_script:
+    - apk update && apk add curl
+  variables:
+    GIT_STRATEGY: none
+  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"
+  only:
+    - master@gitlab-org/gitlab-ce
 
 # Notify slack in the end
 
 notify:slack:
   stage: post-test
   variables:
-    USE_DB: "false"
+    SETUP_DB: "false"
     USE_BUNDLE_INSTALL: "false"
   script:
     - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
@@ -296,3 +380,16 @@ pages:
       - public
   only:
     - master
+
+# Insurance in case a gem needed by one of our releases gets yanked from
+# rubygems.org in the future.
+cache gems:
+  only:
+    - tags
+  variables:
+    SETUP_DB: "false"
+  script:
+    - bundle package --all --all-platforms
+  artifacts:
+    paths:
+      - vendor/cache
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 0000000000000000000000000000000000000000..ac38f0c95218096c9c0da0ea9165ac74495bedbe
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,44 @@
+### Summary
+
+(Summarize the bug encountered concisely)
+
+### Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+### Expected behavior
+
+(What you should see instead)
+
+### Actual behavior
+
+(What actually happens)
+
+### Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+### Output of checks
+
+#### Results of GitLab application Check
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
+
+#### Results of GitLab environment info
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:env:info`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+
+### Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea895ee627519718e6e077f0808e11910f55ab69
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Proposal.md	
@@ -0,0 +1,7 @@
+### Description
+
+(Include problem, use cases, benefits, and/or goals)
+
+### Proposal
+
+### Links / references
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
new file mode 100644
index 0000000000000000000000000000000000000000..9b541aadad1e28d4b30d8eb18b218e257d99a459
--- /dev/null
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -0,0 +1,14 @@
+See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
+
+## What does this MR do?
+
+(briefly describe what this MR is about)
+
+## Moving docs to a new location?
+
+See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
+
+- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
+- [ ] Make sure internal links pointing to the document in question are not broken.
+- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
+- [ ] If working on CE, submit an MR to EE with the changes as well.
diff --git a/.haml-lint.yml b/.haml-lint.yml
new file mode 100644
index 0000000000000000000000000000000000000000..da9a43d9c6dcf23575c20b89e3bfb569d6530749
--- /dev/null
+++ b/.haml-lint.yml
@@ -0,0 +1,103 @@
+# Whether to ignore frontmatter at the beginning of HAML documents for
+# frameworks such as Jekyll/Middleman
+skip_frontmatter: false
+exclude:
+  - 'vendor/**/*'
+  - 'spec/**/*'
+
+linters:
+  AltText:
+    enabled: false
+
+  ClassAttributeWithStaticValue:
+    enabled: false
+
+  ClassesBeforeIds:
+    enabled: false
+
+  ConsecutiveComments:
+    enabled: false
+
+  ConsecutiveSilentScripts:
+    enabled: false
+    max_consecutive: 2
+
+  EmptyObjectReference:
+    enabled: true
+
+  EmptyScript:
+    enabled: true
+
+  FinalNewline:
+    enabled: false
+    present: true
+
+  HtmlAttributes:
+    enabled: false
+
+  ImplicitDiv:
+    enabled: false
+
+  LeadingCommentSpace:
+    enabled: false
+
+  LineLength:
+    enabled: false
+    max: 80
+
+  MultilinePipe:
+    enabled: false
+
+  MultilineScript:
+    enabled: true
+
+  ObjectReferenceAttributes:
+    enabled: true
+
+  RuboCop:
+    enabled: false
+    # These cops are incredibly noisy when it comes to HAML templates, so we
+    # ignore them.
+    ignored_cops:
+      - Lint/BlockAlignment
+      - Lint/EndAlignment
+      - Lint/Void
+      - Metrics/LineLength
+      - Style/AlignParameters
+      - Style/BlockNesting
+      - Style/ElseAlignment
+      - Style/FileName
+      - Style/FinalNewline
+      - Style/FrozenStringLiteralComment
+      - Style/IfUnlessModifier
+      - Style/IndentationWidth
+      - Style/Next
+      - Style/TrailingBlankLines
+      - Style/TrailingWhitespace
+      - Style/WhileUntilModifier
+
+  RubyComments:
+    enabled: false
+
+  SpaceBeforeScript:
+    enabled: false
+
+  SpaceInsideHashAttributes:
+    enabled: false
+    style: space
+
+  Indentation:
+    enabled: true
+    character: space # or tab
+
+  TagName:
+    enabled: true
+
+  TrailingWhitespace:
+    enabled: false
+
+  UnnecessaryInterpolation:
+    enabled: false
+
+  UnnecessaryStringOutput:
+    enabled: false
diff --git a/.pkgr.yml b/.pkgr.yml
index 8fc9fddf8f79d5f5dc391732879b307712d201cf..10bcd7bd4bdf09002cef3f29cf6f0e85551f002a 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -3,6 +3,8 @@ group: git
 services:
   - postgres
 before_precompile: ./bin/pkgr_before_precompile.sh
+env: 
+  - SKIP_STORAGE_VALIDATION=true 
 targets:
   debian-7: &wheezy
     build_dependencies:
@@ -25,6 +27,16 @@ targets:
       - libicu52
       - libpcre3
       - git
+  ubuntu-16.04:
+    build_dependencies:
+      - libkrb5-dev
+      - libicu-dev
+      - cmake
+      - pkg-config
+    dependencies:
+      - libicu55
+      - libpcre3
+      - git
   centos-6:
     build_dependencies:
       - krb5-devel
diff --git a/.rubocop.yml b/.rubocop.yml
index 282f4539f03168608f9383108077623039ebe940..13df3f996139ac85da048dd76d0736a04494154f 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -6,7 +6,7 @@ inherit_from: .rubocop_todo.yml
 
 AllCops:
   TargetRubyVersion: 2.1
-  # Cop names are not displayed in offense messages by default. Change behavior
+  # Cop names are not d§splayed in offense messages by default. Change behavior
   # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
   # option.
   DisplayCopNames: true
@@ -192,6 +192,9 @@ Style/FlipFlop:
 Style/For:
   Enabled: true
 
+# Checks if there is a magic comment to enforce string literals
+Style/FrozenStringLiteralComment:
+  Enabled: false
 # Do not introduce global variables.
 Style/GlobalVars:
   Enabled: true
@@ -450,6 +453,10 @@ Style/VariableName:
   EnforcedStyle: snake_case
   Enabled: true
 
+# Use the configured style when numbering variables.
+Style/VariableNumber:
+  Enabled: false
+
 # Use when x then ... for one-line cases.
 Style/WhenThen:
   Enabled: true
@@ -636,6 +643,10 @@ Lint/RescueException:
 Lint/ShadowedException:
   Enabled: false
 
+# Checks for Object#to_s usage in string interpolation.
+Lint/StringConversionInInterpolation:
+  Enabled: true
+
 # Do not use prefix `_` for a variable that is used.
 Lint/UnderscorePrefixedVariableName:
   Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 20daf1619a7afa4f63eba5a33218eb23defe583f..11b34fafa2ae122534e8ba213e58467c89fab095 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,21 +1,21 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config --exclude-limit 0`
-# on 2016-07-13 12:36:08 -0600 using RuboCop version 0.41.2.
+# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0.
 # 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: 154
+# Offense count: 160
 Lint/AmbiguousRegexpLiteral:
   Enabled: false
 
-# Offense count: 43
+# Offense count: 40
 # Configuration parameters: AllowSafeAssignment.
 Lint/AssignmentInCondition:
   Enabled: false
 
-# Offense count: 14
+# Offense count: 18
 Lint/HandleExceptions:
   Enabled: false
 
@@ -23,117 +23,176 @@ Lint/HandleExceptions:
 Lint/Loop:
   Enabled: false
 
-# Offense count: 15
+# Offense count: 19
 Lint/ShadowingOuterLocalVariable:
   Enabled: false
 
-# Offense count: 3
+# Offense count: 9
+# Cop supports --auto-correct.
+Lint/UnifiedInteger:
+  Enabled: false
+
+# Offense count: 13
 # Cop supports --auto-correct.
-Lint/StringConversionInInterpolation:
+Lint/UnneededSplatExpansion:
   Enabled: false
 
-# Offense count: 44
+# Offense count: 69
 # Cop supports --auto-correct.
 # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
 Lint/UnusedBlockArgument:
   Enabled: false
 
-# Offense count: 129
+# Offense count: 144
 # Cop supports --auto-correct.
 # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
 Lint/UnusedMethodArgument:
   Enabled: false
 
-# Offense count: 12
-# Cop supports --auto-correct.
-Performance/PushSplat:
-  Enabled: false
-
 # Offense count: 2
 # Cop supports --auto-correct.
 Performance/RedundantBlockCall:
   Enabled: false
 
-# Offense count: 4
+# Offense count: 5
 # Cop supports --auto-correct.
 Performance/RedundantMatch:
   Enabled: false
 
-# Offense count: 24
+# Offense count: 26
 # Cop supports --auto-correct.
 # Configuration parameters: MaxKeyValuePairs.
 Performance/RedundantMerge:
   Enabled: false
 
-# Offense count: 60
+# Offense count: 7
+RSpec/BeEql:
+  Enabled: false
+
+# Offense count: 20
+# Configuration parameters: CustomIncludeMethods.
+RSpec/EmptyExampleGroup:
+  Enabled: false
+
+# Offense count: 16
+RSpec/ExpectActual:
+  Enabled: false
+
+# Offense count: 34
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: implicit, each, example
+RSpec/HookArgument:
+  Enabled: false
+
+# Offense count: 168
+RSpec/LeadingSubject:
+  Enabled: false
+
+# Offense count: 162
+RSpec/LetSetup:
+  Enabled: false
+
+# Offense count: 10
+RSpec/MessageChain:
+  Enabled: false
+
+# Offense count: 714
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: allow, expect
+RSpec/MessageExpectation:
+  Enabled: false
+
+# Offense count: 2423
+RSpec/MultipleExpectations:
+  Max: 36
+
+# Offense count: 1504
+RSpec/NamedSubject:
+  Enabled: false
+
+# Offense count: 1335
+# Configuration parameters: MaxNesting.
+RSpec/NestedGroups:
+  Enabled: false
+
+# Offense count: 99
+RSpec/SubjectStub:
+  Enabled: false
+
+# Offense count: 64
 Rails/OutputSafety:
   Enabled: false
 
-# Offense count: 128
+# Offense count: 151
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: strict, flexible
 Rails/TimeZone:
   Enabled: false
 
-# Offense count: 12
+# Offense count: 15
 # Cop supports --auto-correct.
 # Configuration parameters: Include.
 # Include: app/models/**/*.rb
 Rails/Validation:
   Enabled: false
 
-# Offense count: 217
+# Offense count: 2
+# Cop supports --auto-correct.
+Security/JSONLoad:
+  Enabled: false
+
+# Offense count: 284
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
 # SupportedStyles: with_first_parameter, with_fixed_indentation
 Style/AlignParameters:
   Enabled: false
 
-# Offense count: 32
+# Offense count: 28
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: always, conditionals
 Style/AndOr:
   Enabled: false
 
-# Offense count: 47
+# Offense count: 52
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: percent_q, bare_percent
 Style/BarePercentLiterals:
   Enabled: false
 
-# Offense count: 258
+# Offense count: 291
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: braces, no_braces, context_dependent
 Style/BracesAroundHashParameters:
   Enabled: false
 
-# Offense count: 5
+# Offense count: 6
 Style/CaseEquality:
   Enabled: false
 
-# Offense count: 19
+# Offense count: 26
 # Cop supports --auto-correct.
 Style/ColonMethodCall:
   Enabled: false
 
-# Offense count: 3
+# Offense count: 2
 # Cop supports --auto-correct.
 # Configuration parameters: Keywords.
 # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
 Style/CommentAnnotation:
   Enabled: false
 
-# Offense count: 34
+# Offense count: 30
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
 # SupportedStyles: assign_to_condition, assign_inside_condition
 Style/ConditionalAssignment:
   Enabled: false
 
-# Offense count: 789
+# Offense count: 957
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: leading, trailing
@@ -144,11 +203,12 @@ Style/DotPosition:
 Style/DoubleNegation:
   Enabled: false
 
-# Offense count: 3
+# Offense count: 6
+# Cop supports --auto-correct.
 Style/EachWithObject:
   Enabled: false
 
-# Offense count: 30
+# Offense count: 26
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: empty, nil, both
@@ -160,19 +220,19 @@ Style/EmptyElse:
 Style/EmptyLiteral:
   Enabled: false
 
-# Offense count: 123
+# Offense count: 140
 # Cop supports --auto-correct.
 # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
 Style/ExtraSpacing:
   Enabled: false
 
-# Offense count: 7
+# Offense count: 6
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: format, sprintf, percent
 Style/FormatString:
   Enabled: false
 
-# Offense count: 48
+# Offense count: 201
 # Configuration parameters: MinBodyLength.
 Style/GuardClause:
   Enabled: false
@@ -181,73 +241,84 @@ Style/GuardClause:
 Style/IfInsideElse:
   Enabled: false
 
-# Offense count: 177
+# Offense count: 174
 # Cop supports --auto-correct.
 # Configuration parameters: MaxLineLength.
 Style/IfUnlessModifier:
   Enabled: false
 
-# Offense count: 52
+# Offense count: 53
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
 # SupportedStyles: special_inside_parentheses, consistent, align_brackets
 Style/IndentArray:
   Enabled: false
 
-# Offense count: 89
+# Offense count: 95
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
 # SupportedStyles: special_inside_parentheses, consistent, align_braces
 Style/IndentHash:
   Enabled: false
 
-# Offense count: 12
+# Offense count: 29
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: line_count_dependent, lambda, literal
 Style/Lambda:
   Enabled: false
 
-# Offense count: 6
+# Offense count: 5
 # Cop supports --auto-correct.
 Style/LineEndConcatenation:
   Enabled: false
 
-# Offense count: 13
+# Offense count: 15
 # Cop supports --auto-correct.
 Style/MethodCallParentheses:
   Enabled: false
 
-# Offense count: 62
+# Offense count: 8
+Style/MethodMissing:
+  Enabled: false
+
+# Offense count: 95
 # Cop supports --auto-correct.
 Style/MutableConstant:
   Enabled: false
 
-# Offense count: 10
+# Offense count: 8
 # Cop supports --auto-correct.
 Style/NestedParenthesizedCalls:
   Enabled: false
 
-# Offense count: 12
+# Offense count: 13
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
 # SupportedStyles: skip_modifier_ifs, always
 Style/Next:
   Enabled: false
 
-# Offense count: 8
+# Offense count: 12
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
 # SupportedOctalStyles: zero_with_o, zero_only
 Style/NumericLiteralPrefix:
   Enabled: false
 
+# Offense count: 53
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: predicate, comparison
+Style/NumericPredicate:
+  Enabled: false
+
 # Offense count: 29
 # Cop supports --auto-correct.
 Style/ParallelAssignment:
   Enabled: false
 
-# Offense count: 208
+# Offense count: 294
 # Cop supports --auto-correct.
 # Configuration parameters: PreferredDelimiters.
 Style/PercentLiteralDelimiters:
@@ -265,7 +336,7 @@ Style/PercentQLiterals:
 Style/PerlBackrefs:
   Enabled: false
 
-# Offense count: 32
+# Offense count: 38
 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
 # NamePrefix: is_, has_, have_
 # NamePrefixBlacklist: is_, has_, have_
@@ -273,7 +344,7 @@ Style/PerlBackrefs:
 Style/PredicateName:
   Enabled: false
 
-# Offense count: 28
+# Offense count: 26
 # Cop supports --auto-correct.
 Style/PreferredHashMethods:
   Enabled: false
@@ -283,14 +354,14 @@ Style/PreferredHashMethods:
 Style/Proc:
   Enabled: false
 
-# Offense count: 20
+# Offense count: 22
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: compact, exploded
 Style/RaiseArgs:
   Enabled: false
 
-# Offense count: 3
+# Offense count: 4
 # Cop supports --auto-correct.
 Style/RedundantBegin:
   Enabled: false
@@ -300,29 +371,34 @@ Style/RedundantBegin:
 Style/RedundantException:
   Enabled: false
 
-# Offense count: 23
+# Offense count: 24
 # Cop supports --auto-correct.
 Style/RedundantFreeze:
   Enabled: false
 
-# Offense count: 377
+# Offense count: 427
 # Cop supports --auto-correct.
 Style/RedundantSelf:
   Enabled: false
 
-# Offense count: 94
+# Offense count: 97
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
 # SupportedStyles: slashes, percent_r, mixed
 Style/RegexpLiteral:
   Enabled: false
 
-# Offense count: 17
+# Offense count: 18
 # Cop supports --auto-correct.
 Style/RescueModifier:
   Enabled: false
 
-# Offense count: 2
+# Offense count: 114
+# Cop supports --auto-correct.
+Style/SafeNavigation:
+  Enabled: false
+
+# Offense count: 7
 # Cop supports --auto-correct.
 Style/SelfAssignment:
   Enabled: false
@@ -339,70 +415,77 @@ Style/SingleLineBlockParams:
 Style/SingleLineMethods:
   Enabled: false
 
-# Offense count: 119
+# Offense count: 125
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: space, no_space
 Style/SpaceBeforeBlockBraces:
   Enabled: false
 
-# Offense count: 11
+# Offense count: 10
 # Cop supports --auto-correct.
 # Configuration parameters: AllowForAlignment.
 Style/SpaceBeforeFirstArg:
   Enabled: false
 
-# Offense count: 130
+# Offense count: 145
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
 # SupportedStyles: space, no_space
 Style/SpaceInsideBlockBraces:
   Enabled: false
 
-# Offense count: 98
+# Offense count: 99
 # Cop supports --auto-correct.
 Style/SpaceInsideBrackets:
   Enabled: false
 
-# Offense count: 60
+# Offense count: 65
 # Cop supports --auto-correct.
 Style/SpaceInsideParens:
   Enabled: false
 
-# Offense count: 5
+# Offense count: 7
 # Cop supports --auto-correct.
 Style/SpaceInsidePercentLiteralDelimiters:
   Enabled: false
 
-# Offense count: 36
+# Offense count: 41
 # Cop supports --auto-correct.
 # Configuration parameters: SupportedStyles.
 # SupportedStyles: use_perl_names, use_english_names
 Style/SpecialGlobalVars:
   EnforcedStyle: use_perl_names
 
-# Offense count: 30
+# Offense count: 31
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 # SupportedStyles: single_quotes, double_quotes
 Style/StringLiteralsInInterpolation:
   Enabled: false
 
-# Offense count: 24
+# Offense count: 33
 # Cop supports --auto-correct.
 # Configuration parameters: IgnoredMethods.
 # IgnoredMethods: respond_to, define_method
 Style/SymbolProc:
   Enabled: false
 
-# Offense count: 23
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
+# SupportedStyles: require_parentheses, require_no_parentheses
+Style/TernaryParentheses:
+  Enabled: false
+
+# Offense count: 29
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
 # SupportedStyles: comma, consistent_comma, no_comma
 Style/TrailingCommaInArguments:
   Enabled: false
 
-# Offense count: 113
+# Offense count: 102
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
 # SupportedStyles: comma, consistent_comma, no_comma
@@ -415,7 +498,7 @@ Style/TrailingCommaInLiteral:
 Style/TrailingUnderscoreVariable:
   Enabled: false
 
-# Offense count: 90
+# Offense count: 76
 # Cop supports --auto-correct.
 Style/TrailingWhitespace:
   Enabled: false
@@ -427,12 +510,12 @@ Style/TrailingWhitespace:
 Style/TrivialAccessors:
   Enabled: false
 
-# Offense count: 3
+# Offense count: 2
 # Cop supports --auto-correct.
 Style/UnlessElse:
   Enabled: false
 
-# Offense count: 13
+# Offense count: 14
 # Cop supports --auto-correct.
 Style/UnneededInterpolation:
   Enabled: false
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 66f9975d4ce50b31f6f06376c5ef5282aa4c34f8..aae8d9b6dbe023bf2522e8372a6198711db176f2 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -61,7 +61,7 @@ linters:
   
   # Separate rule, function, and mixin declarations with empty lines.
   EmptyLineBetweenBlocks:
-    enabled: false
+    enabled: true
   
   # Reports when you have an empty rule set.
   EmptyRule:
@@ -79,7 +79,7 @@ linters:
   
   # HEX colors should use three-character values where possible.
   HexLength:
-    enabled: true
+    enabled: false
   
   # HEX color values should use lower-case colors to differentiate between
   # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
@@ -143,7 +143,7 @@ linters:
   # with two colons. Pseudo-classes, like :hover and :first-child, should
   # be declared with one colon.
   PseudoElement:
-    enabled: false
+    enabled: true
   
   # Avoid qualifying elements in selectors (also known as "tag-qualifying").
   QualifyingElement:
@@ -172,7 +172,7 @@ linters:
   # Split selectors onto separate lines after each comma, and have each
   # individual selector occupy a single line.
   SingleLinePerSelector:
-    enabled: false
+    enabled: true
   
   # Commas in lists should be followed by a space.
   SpaceAfterComma:
@@ -191,7 +191,7 @@ linters:
   # Variables should be formatted with a single space separating the colon
   # from the variable's value.
   SpaceAfterVariableColon:
-    enabled: false
+    enabled: true
 
   # Variables should be formatted with no space between the name and the
   # colon.
@@ -201,7 +201,7 @@ linters:
   # Operators should be formatted with a single space on both sides of an
   # infix operator.
   SpaceAroundOperator:
-    enabled: false
+    enabled: true
   
   # Opening braces should be preceded by a single space.
   SpaceBeforeBrace:
@@ -219,11 +219,11 @@ linters:
   # Property values, @extend, @include, and @import directives, and variable
   # declarations should always end with a semicolon.
   TrailingSemicolon:
-    enabled: false
+    enabled: true
 
   # Reports lines containing trailing whitespace.
   TrailingWhitespace:
-    enabled: false
+    enabled: true
 
   # Don't write trailing zeros for numeric values with a decimal point.
   TrailingZero:
diff --git a/.vagrant_enabled b/.vagrant_enabled
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/CHANGELOG b/CHANGELOG.md
similarity index 57%
rename from CHANGELOG
rename to CHANGELOG.md
index d3d0f79039f5d2af4daa2e009deec123c1902371..e77cda75ccb9b685223d98d41b6e2034bf9c26f3 100644
--- a/CHANGELOG
+++ b/CHANGELOG.md
@@ -1,36 +1,738 @@
-Please view this file on the master branch, on stable branches it's out of date.
-
-v 8.11.0 (unreleased)
+**Note:** This file is automatically generated. Please see the [developer
+documentation](doc/development/changelog.md) for instructions on adding your own
+entry.
+
+## 8.14.0 (2016-11-22)
+
+- Use separate email-token for incoming email and revert back the inactive feature. !5914
+- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps)
+- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342
+- Finer-grained Git gargage collection. !6588
+- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601
+- Process commits using a dedicated Sidekiq worker. !6802
+- Fix showing pipeline status for a given commit from correct branch. !7034
+- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta)
+- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps)
+- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda)
+- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato)
+- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato)
+- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger)
+- Only skip group when it's actually a group in the "Share with group" select. !7262
+- Introduce round-robin project creation to spread load over multiple shards. !7266
+- Ensure merge request's "remove branch" accessors return booleans. !7267
+- Expose label IDs in API. !7275 (Rares Sfirlogea)
+- Fix invalid filename validation on eslint. !7281
+- API: Ability to retrieve version information. !7286 (Robert Schilling)
+- Set default Sidekiq retries to 3. !7294
+- Return 400 when creating a system hook fails. !7350 (Robert Schilling)
+- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright)
+- Add an index for project_id in project_import_data to improve performance.
+- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose)
+- Faster search inside Project.
+- Clicking "force remove source branch" label now toggles the checkbox again.
+- Allow to test JIRA service settings without having a repository.
+- Fix: Guest sees some repository details and gets 404.
+- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2.
+- Fix: Todos Filter Shows All Users.
+- Fix broken commits search.
+- Show correct environment log in admin/logs (@duk3luk3 !7191)
+- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
+- Diff collapse won't shift when collapsing.
+- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814
+- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
+- Trim leading and trailing whitespace on project_path (Linus Thiel)
+- Prevent award emoji via notes for issues/MRs authored by user (barthc)
+- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
+- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand)
+- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
+- Fix Markdown styling inside reference links (Jan Zdráhal)
+- Create new issue board list after creating a new label
+- Fix extra space on Build sidebar on Firefox !7060
+- Fail gracefully when creating merge request with non-existing branch (alexsanford)
+- Fix mobile layout issues in admin user overview page !7087
+- Fix HipChat notifications rendering (airatshigapov, eisnerd)
+- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
+- Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
+- Refactor Jira service to use jira-ruby gem
+- Improved todos empty state
+- Add hover to trash icon in notes !7008 (blackst0ne)
+- Hides project activity tabs when features are disabled
+- Only show one error message for an invalid email !5905 (lycoperdon)
+- Added guide describing how to upgrade PostgreSQL using Slony
+- Fix sidekiq stats in admin area (blackst0ne)
+- Added label description as tooltip to issue board list title
+- Created cycle analytics bundle JavaScript file
+- Hides container registry when repository is disabled
+- API: Fix booleans not recognized as such when using the `to_boolean` helper
+- Removed delete branch tooltip !6954
+- Stop unauthorized users dragging on milestone page (blackst0ne)
+- Restore issue boards welcome message when a project is created !6899
+- Check that JavaScript file names match convention !7238 (winniehell)
+- Do not show tooltip for active element !7105 (winniehell)
+- Escape ref and path for relative links !6050 (winniehell)
+- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
+- Fix broken issue/merge request links in JIRA comments. !6143 (Brian Kintz)
+- Fix filtering of milestones with quotes in title (airatshigapov)
+- Fix issue boards dragging bug in Safari
+- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
+- Update mail_room and enable sentinel support to Reply By Email (!7101)
+- Add task completion status in Issues and Merge Requests tabs: "X of Y tasks completed" (!6527, @gmesalazar)
+- Simpler arguments passed to named_route on toggle_award_url helper method
+- Fix typo in framework css class. !7086 (Daniel Voogsgerd)
+- New issue board list dropdown stays open after adding a new list
+- Fix: Backup restore doesn't clear cache
+- Optimize Event queries by removing default order
+- Add new icon for skipped builds
+- Show created icon in pipeline mini-graph
+- Remove duplicate links from sidebar
+- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
+- Add Rake task to create/repair GitLab Shell hooks symlinks !5634
+- Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld)
+- Replace jquery.cookie plugin with js.cookie !7085
+- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
+- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
+- Show full status link on MR & commit pipelines
+- Fix documents and comments on Build API `scope`
+- Initialize Sidekiq with the list of queues used by GitLab
+- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
+- Shortened merge request modal to let clipboard button not overlap
+- Adds JavaScript validation for group path editing field
+- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
+- Improve search query parameter naming in /admin/users !7115 (YarNayar)
+- Fix table pagination to be responsive
+- Fix applying GitHub-imported labels when importing job is interrupted
+- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
+- Updated commit SHA styling on the branches page.
+- Fix 404 when visit /projects page
+
+## 8.13.5 (2016-11-08)
+
+- Restore unauthenticated access to public container registries
+- Fix showing pipeline status for a given commit from correct branch. !7034
+- Only skip group when it's actually a group in the "Share with group" select. !7262
+- Introduce round-robin project creation to spread load over multiple shards. !7266
+- Ensure merge request's "remove branch" accessors return booleans. !7267
+- Ensure external users are not able to clone disabled repositories.
+- Fix XSS issue in Markdown autolinker.
+- Respect event visibility in Gitlab::ContributionsCalendar.
+- Honour issue and merge request visibility in their respective finders.
+- Disable reference Markdown for unavailable features.
+- Fix lightweight tags not processed correctly by GitTagPushService. !6532
+- Allow owners to fetch source code in CI builds. !6943
+- Return conflict error in label API when title is taken by group label. !7014
+- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123
+- Fix builds tab visibility. !7178
+- Fix project features default values. !7181
+
+## 8.13.4
+
+- Pulled due to packaging error.
+
+## 8.13.3 (2016-11-02)
+
+- Removes any symlinks before importing a project export file. CVE-2016-9086
+- Fixed Import/Export foreign key issue to do with project members.
+- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
+- Changed build dropdown list length to be 6,5 builds long in the pipeline graph
+
+## 8.13.2 (2016-10-31)
+
+- Fix encoding issues on pipeline commits. !6832
+- Use Hash rocket syntax to fix cycle analytics under Ruby 2.1. !6977
+- Modify GitHub importer to be retryable. !7003
+- Fix refs dropdown selection with special characters. !7061
+- Fix horizontal padding for highlight blocks. !7062
+- Pass user instance to `Labels::FindOrCreateService` or `skip_authorization: true`. !7093
+- Fix builds dropdown overlapping bug. !7124
+- Fix applying labels for GitHub-imported MRs. !7139
+- Fix importing MR comments from GitHub. !7139
+- Fix project member access for group links. !7144
+- API: Fix booleans not recognized as such when using the `to_boolean` helper. !7149
+- Fix and improve `Sortable.highest_label_priority`. !7165
+- Fixed sticky merge request tabs when sidebar is pinned. !7167
+- Only remove right connector of first build of last stage. !7179
+
+## 8.13.1 (2016-10-25)
+
+- Fix branch protection API. !6215
+- Fix hidden pipeline graph on commit and MR page. !6895
+- Fix Cycle analytics not showing correct data when filtering by date. !6906
+- Ensure custom provider tab labels don't break layout. !6993
+- Fix issue boards user link when in subdirectory. !7018
+- Refactor and add new environment functionality to CI yaml reference. !7026
+- Fix typo in project settings that prevents users from enabling container registry. !7037
+- Fix events order in `users/:id/events` endpoint. !7039
+- Remove extra line for empty issue description. !7045
+- Don't append issue/MR templates to any existing text. !7050
+- Fix error in generating labels. !7055
+- Stop clearing the database cache on `rake cache:clear`. !7056
+- Only show register tab if signup enabled. !7058
+- Fix lightweight tags not processed correctly by GitTagPushService
+- Expire and build repository cache after project import. !7064
+- Fix bug where labels would be assigned to issues that were moved. !7065
+- Fix reply-by-email not working due to queue name mismatch. !7068
+- Fix 404 for group pages when GitLab setup uses relative url. !7071
+- Fix `User#to_reference`. !7088
+- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094
+- Fix unauthorized users dragging on issue boards. !7096
+- Only schedule `ProjectCacheWorker` jobs when needed. !7099
+
+## 8.13.0 (2016-10-22)
+
+- Fix save button on project pipeline settings page. (!6955)
+- All Sidekiq workers now use their own queue
+- Avoid race condition when asynchronously removing expired artifacts. (!6881)
+- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
+- Respond with 404 Not Found for non-existent tags (Linus Thiel)
+- Truncate long labels with ellipsis in labels page
+- Improve tabbing usability for sign in page (ClemMakesApps)
+- Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
+- Adding members no longer silently fails when there is extra whitespace
+- Update runner version only when updating contacted_at
+- Add link from system note to compare with previous version
+- Use gitlab-shell v3.6.6
+- Ignore references to internal issues when using external issues tracker
+- Ability to resolve merge request conflicts with editor !6374
+- Add `/projects/visible` API endpoint (Ben Boeckel)
+- Fix centering of custom header logos (Ashley Dumaine)
+- Keep around commits only pipeline creation as pipeline data doesn't change over time
+- Update duration at the end of pipeline
+- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+- Add group level labels. (!6425)
+- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+- Cancelled pipelines could be retried. !6927
+- Updating verbiage on git basics to be more intuitive
+- Fix project_feature record not generated on project creation
+- Clarify documentation for Runners API (Gennady Trafimenkov)
+- Use optimistic locking for pipelines and builds
+- The instrumentation for Banzai::Renderer has been restored
+- Change user & group landing page routing from /u/:username to /:username
+- Added documentation for .gitattributes files
+- Move Pipeline Metrics to separate worker
+- AbstractReferenceFilter caches project_refs on RequestStore when active
+- Replaced the check sign to arrow in the show build view. !6501
+- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
+- ProjectCacheWorker updates caches at most once per 15 minutes per project
+- Fix Error 500 when viewing old merge requests with bad diff data
+- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
+- Fix viewing merged MRs when the source project has been removed !6991
+- Speed-up group milestones show page
+- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
+- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
+- Fix discussion thread from emails for merge requests. !7010
+- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+- Add tag shortcut from the Commit page. !6543
+- Keep refs for each deployment
+- Close open tooltips on page navigation (Linus Thiel)
+- Allow browsing branches that end with '.atom'
+- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
+- Add more tests for calendar contribution (ClemMakesApps)
+- Update Gitlab Shell to fix some problems with moving projects between storages
+- Cache rendered markdown in the database, rather than Redis
+- Add todo toggle event (ClemMakesApps)
+- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+- Simplify Mentionable concern instance methods
+- API: Ability to retrieve version information (Robert Schilling)
+- Fix permission for setting an issue's due date
+- API: Multi-file commit !6096 (mahcsig)
+- Unicode emoji are now converted to images
+- Revert "Label list shows all issues (opened or closed) with that label"
+- Expose expires_at field when sharing project on API
+- Fix VueJS template tags being rendered in code comments
+- Added copy file path button to merge request diff files
+- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+- Add Issue Board API support (andrebsguedes)
+- Allow the Koding integration to be configured through the API
+- Add new issue button to each list on Issues Board
+- Execute specific named route method from toggle_award_url helper method
+- Added soft wrap button to repository file/blob editor
+- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
+- Show the time ago a merge request was deployed to an environment
+- Add RTL support to markdown renderer (Ebrahim Byagowi)
+- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+- Fix todos page mobile viewport layout (ClemMakesApps)
+- Make issues search less finicky
+- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
+- Remove redundant mixins (ClemMakesApps)
+- Added 'Download' button to the Snippets page (Justin DiPierro)
+- Add visibility level to project repository
+- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+- Fix showing commits from source project for merge request !6658
+- Fix that manual jobs would no longer block jobs in the next stage. !6604
+- Add configurable email subject suffix (Fu Xu)
+- Use defined colour for a language when available !6748 (nilsding)
+- Added tooltip to fork count on project show page. (Justin DiPierro)
+- Use a ConnectionPool for Rails.cache on Sidekiq servers
+- Replace `alias_method_chain` with `Module#prepend`
+- Enable GitLab Import/Export for non-admin users.
+- Preserve label filters when sorting !6136 (Joseph Frazier)
+- MergeRequest#new form load diff asynchronously
+- Only update issuable labels if they have been changed
+- Take filters in account in issuable counters. !6496
+- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+- Replace static issue fixtures by script !6059 (winniehell)
+- Append issue template to existing description !6149 (Joseph Frazier)
+- Trending projects now only show public projects and the list of projects is cached for a day
+- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
+- Revoke button in Applications Settings underlines on hover.
+- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
+- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+- Revert avoid touching file system on Build#artifacts?
+- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
+- Add disabled delete button to protected branches (ClemMakesApps)
+- Add broadcast messages and alerts below sub-nav
+- Better empty state for Groups view
+- API: New /users/:id/events endpoint
+- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
+- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
+- Add organization field to user profile
+- Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
+- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
+- Fix deploy status responsiveness error !6633
+- Make searching for commits case insensitive
+- Fix resolved discussion display in side-by-side diff view !6575
+- Optimize GitHub importing for speed and memory
+- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
+- Notify the Merger about merge after successful build (Dimitris Karakasilis)
+- Reduce queries needed to find users using their SSH keys when pushing commits
+- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
+- Fix broken repository 500 errors in project list
+- Fix the diff in the merge request view when converting a symlink to a regular file
+- Fix Pipeline list commit column width should be adjusted
+- Close todos when accepting merge requests via the API !6486 (tonygambone)
+- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
+- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+- Retouch environments list and deployments list
+- Add multiple command support for all label related slash commands !6780 (barthc)
+- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
+- Allow empty merge requests !6384 (Artem Sidorenko)
+- Grouped pipeline dropdown is a scrollable container
+- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
+- Fixes padding in all clipboard icons that have .btn class
+- Fix a typo in doc/api/labels.md
+- Fix double-escaping in activities tab (Alexandre Maia)
+- API: all unknown routing will be handled with 404 Not Found
+- Add docs for request profiling
+- Delete dynamic environments
+- Fix buggy iOS tooltip layering behavior.
+- Make guests unable to view MRs on private projects
+- Fix broken Project API docs (Takuya Noguchi)
+- Migrate invalid project members (owner -> master)
+
+## 8.12.9 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
+## 8.12.8 (2016-11-02)
+
+- Removes any symlinks before importing a project export file. CVE-2016-9086
+- Fixed Import/Export foreign key issue to do with project members.
+
+## 8.12.7
+
+  - Prevent running `GfmAutocomplete` setup for each diff note. !6569
+  - Fix long commit messages overflow viewport in file tree. !6573
+  - Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659
+  - Prevent flash alert text from being obscured when container is fluid. !6694
+  - Fix due date being displayed as `NaN` in Safari. !6797
+  - Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812
+  - Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817
+  - Fix GFM autocomplete setup being called several times. !6840
+  - Handle case where deployment ref no longer exists. !6855
+
+## 8.12.6
+
+  - Update mailroom to 0.8.1 in Gemfile.lock  !6814
+
+## 8.12.5
+
+  - Switch from request to env in ::API::Helpers. !6615
+  - Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
+  - Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
+  - Add a new gitlab:users:clear_all_authentication_tokens task. !6745
+  - Don't send Private-Token (API authentication) headers to Sentry
+  - Share projects via the API only with groups the authenticated user can access
+
+## 8.12.4
+
+  - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
+  - Fix padding in build sidebar. !6506
+  - Changed compare dropdowns to dropdowns with isolated search input. !6550
+  - Fix race condition on LFS Token. !6592
+  - Fix type mismatch bug when closing Jira issue. !6619
+  - Fix lint-doc error. !6623
+  - Skip wiki creation when GitHub project has wiki enabled. !6665
+  - Fix issues importing services via Import/Export. !6667
+  - Restrict failed login attempts for users with 2FA enabled. !6668
+  - Fix failed project deletion when feature visibility set to private. !6688
+  - Prevent claiming associated model IDs via import.
+  - Set GitLab project exported file permissions to owner only
+  - Improve the way merge request versions are compared with each other
+
+## 8.12.3
+
+  - Update Gitlab Shell to support low IO priority for storage moves
+
+## 8.12.2
+
+  - Fix Import/Export not recognising correctly the imported services.
+  - Fix snippets pagination
+  - Fix "Create project" button layout when visibility options are restricted
+  - Fix List-Unsubscribe header in emails
+  - Fix IssuesController#show degradation including project on loaded notes
+  - Fix an issue with the "Commits" section of the cycle analytics summary. !6513
+  - Fix errors importing project feature and milestone models using GitLab project import
+  - Make JWT messages Docker-compatible
+  - Fix duplicate branch entry in the merge request version compare dropdown
+  - Respect the fork_project permission when forking projects
+  - Only update issuable labels if they have been changed
+  - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
+  - Fix resolve discussion buttons endpoint path
+  - Refactor remnants of CoffeeScript destructured opts and super !6261
+
+## 8.12.1
+
+  - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
+  - Fix issue with search filter labels not displaying
+
+## 8.12.0 (2016-09-22)
+
+  - Removes inconsistency regarding tagging immediatelly as merged once you create a new branch. !6408
+  - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
+  - Only check :can_resolve permission if the note is resolvable
+  - Bump fog-aws to v0.11.0 to support ap-south-1 region
+  - Add ability to fork to a specific namespace using API. (ritave)
+  - Allow to set request_access_enabled for groups and projects
+  - Cleanup misalignments in Issue list view !6206
+  - Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
+  - Add Pipelines for Commit
+  - Prune events older than 12 months. (ritave)
+  - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
+  - Fix issues/merge-request templates dropdown for forked projects
+  - Filter tags by name !6121
+  - Update gitlab shell secret file also when it is empty. !3774 (glensc)
+  - Give project selection dropdowns responsive width, make non-wrapping.
+  - Fix note form hint showing slash commands supported for commits.
+  - Make push events have equal vertical spacing.
+  - API: Ensure invitees are not returned in Members API.
+  - Preserve applied filters on issues search.
+  - Add two-factor recovery endpoint to internal API !5510
+  - Pass the "Remember me" value to the U2F authentication form
+  - Display stages in valid order in stages dropdown on build page
+  - Only update projects.last_activity_at once per hour when creating a new event
+  - Cycle analytics (first iteration) !5986
+  - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
+  - Move pushes_since_gc from the database to Redis
+  - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags
+  - Add font color contrast to external label in admin area (ClemMakesApps)
+  - Fix find file navigation links (ClemMakesApps)
+  - Change logo animation to CSS (ClemMakesApps)
+  - Instructions for enabling Git packfile bitmaps !6104
+  - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
+  - Fix long comments in diffs messing with table width
+  - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
+  - Fix pagination on user snippets page
+  - Honor "fixed layout" preference in more places !6422
+  - Run CI builds with the permissions of users !5735
+  - Fix sorting of issues in API
+  - Fix download artifacts button links !6407
+  - Sort project variables by key. !6275 (Diego Souza)
+  - Ensure specs on sorting of issues in API are deterministic on MySQL
+  - Added ability to use predefined CI variables for environment name
+  - Added ability to specify URL in environment configuration in gitlab-ci.yml
+  - Escape search term before passing it to Regexp.new !6241 (winniehell)
+  - Fix pinned sidebar behavior in smaller viewports !6169
+  - Fix file permissions change when updating a file on the Gitlab UI !5979
+  - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev)
+  - Change merge_error column from string to text type
+  - Fix issue with search filter labels not displaying
+  - Reduce contributions calendar data payload (ClemMakesApps)
+  - Show all pipelines for merge requests even from discarded commits !6414
+  - Replace contributions calendar timezone payload with dates (ClemMakesApps)
+  - Changed MR widget build status to pipeline status !6335
+  - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
+  - Enable pipeline events by default !6278
+  - Add pipeline email service !6019
+  - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
+  - Added go to issue boards keyboard shortcut
+  - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
+  - Emoji can be awarded on Snippets !4456
+  - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+  - Fix blame table layout width
+  - Spec testing if issue authors can read issues on private projects
+  - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
+  - Request only the LDAP attributes we need !6187
+  - Center build stage columns in pipeline overview (ClemMakesApps)
+  - Fix bug with tooltip not hiding on discussion toggle button
+  - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+  - Fix bug stopping issue description being scrollable after selecting issue template
+  - Remove suggested colors hover underline (ClemMakesApps)
+  - Fix jump to discussion button being displayed on commit notes
+  - Shorten task status phrase (ClemMakesApps)
+  - Fix project visibility level fields on settings
+  - Add hover color to emoji icon (ClemMakesApps)
+  - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
+  - Add textarea autoresize after comment (ClemMakesApps)
+  - Do not write SSH public key 'comments' to authorized_keys !6381
+  - Add due date to issue todos
+  - Refresh todos count cache when an Issue/MR is deleted
+  - Fix branches page dropdown sort alignment (ClemMakesApps)
+  - Hides merge request button on branches page is user doesn't have permissions
+  - Add white background for no readme container (ClemMakesApps)
+  - API: Expose issue confidentiality flag. (Robert Schilling)
+  - Fix markdown anchor icon interaction (ClemMakesApps)
+  - Test migration paths from 8.5 until current release !4874
+  - Replace animateEmoji timeout with eventListener (ClemMakesApps)
+  - Show badges in Milestone tabs. !5946 (Dan Rowden)
+  - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+  - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
+  - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
+  - Remove Gitorious import
+  - Loads GFM autocomplete source only when required
+  - Fix issue with slash commands not loading on new issue page
+  - Fix inconsistent background color for filter input field (ClemMakesApps)
+  - Remove prefixes from transition CSS property (ClemMakesApps)
+  - Add Sentry logging to API calls
+  - Add BroadcastMessage API
+  - Merge request tabs are fixed when scrolling page
+  - Use 'git update-ref' for safer web commits !6130
+  - Sort pipelines requested through the API
+  - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+  - Fix issue boards loading on large screens
+  - Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
+  - Show queued time when showing a pipeline !6084
+  - Remove unused mixins (ClemMakesApps)
+  - Fix issue board label filtering appending already filtered labels
+  - Add search to all issue board lists
+  - Scroll active tab into view on mobile
+  - Fix groups sort dropdown alignment (ClemMakesApps)
+  - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
+  - Use JavaScript tooltips for mentions !5301 (winniehell)
+  - Add hover state to todos !5361 (winniehell)
+  - Fix icon alignment of star and fork buttons !5451 (winniehell)
+  - Fix alignment of icon buttons !5887 (winniehell)
+  - Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
+  - Fix markdown help references (ClemMakesApps)
+  - Add last commit time to repo view (ClemMakesApps)
+  - Fix accessibility and visibility of project list dropdown button !6140
+  - Fix missing flash messages on service edit page (airatshigapov)
+  - Added project-specific enable/disable setting for LFS !5997
+  - Added group-specific enable/disable setting for LFS !6164
+  - Add optional 'author' param when making commits. !5822 (dandunckelman)
+  - Don't expose a user's token in the `/api/v3/user` API (!6047)
+  - Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
+  - Ability to manage project issues, snippets, wiki, merge requests and builds access level
+  - Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
+  - Align add button on repository view (ClemMakesApps)
+  - Fix contributions calendar month label truncation (ClemMakesApps)
+  - Import release note descriptions from GitHub (EspadaV8)
+  - Added tests for diff notes
+  - Add pipeline events to Slack integration !5525
+  - Add a button to download latest successful artifacts for branches and tags !5142
+  - Remove redundant pipeline tooltips (ClemMakesApps)
+  - Expire commit info views after one day, instead of two weeks, to allow for user email updates
+  - Add delimiter to project stars and forks count (ClemMakesApps)
+  - Fix badge count alignment (ClemMakesApps)
+  - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
+  - Fix repo title alignment (ClemMakesApps)
+  - Change update interval of contacted_at
+  - Add LFS support to SSH !6043
+  - Fix branch title trailing space on hover (ClemMakesApps)
+  - Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
+  - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+  - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
+  - Order award emoji tooltips in order they were added (EspadaV8)
+  - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
+  - Update merge_requests.md with a simpler way to check out a merge request. !5944
+  - Fix button missing type (ClemMakesApps)
+  - Gitlab::Checks is now instrumented
+  - Move to project dropdown with infinite scroll for better performance
+  - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz)
+  - Load branches asynchronously in Cherry Pick and Revert dialogs.
+  - Convert datetime coffeescript spec to ES6 (ClemMakesApps)
+  - Add merge request versions !5467
+  - Change using size to use count and caching it for number of group members. !5935
+  - Replace play icon font with svg (ClemMakesApps)
+  - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
+  - Reduce number of database queries on builds tab
+  - Wrap text in commit message containers
+  - Capitalize mentioned issue timeline notes (ClemMakesApps)
+  - Fix inconsistent checkbox alignment (ClemMakesApps)
+  - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
+  - Adds response mime type to transaction metric action when it's not HTML
+  - Fix hover leading space bug in pipeline graph !5980
+  - Avoid conflict with admin labels when importing GitHub labels
+  - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
+  - Fix repository page ui issues
+  - Avoid protected branches checks when verifying access without branch name
+  - Add information about user and manual build start to runner as variables !6201 (Sergey Gnuskov)
+  - Fixed invisible scroll controls on build page on iPhone
+  - Fix error on raw build trace download for old builds stored in database !4822
+  - Refactor the triggers page and documentation !6217
+  - Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
+  - Use default clone protocol on "check out, review, and merge locally" help page URL
+  - Let the user choose a namespace and name on GitHub imports
+  - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
+  - Allow bulk update merge requests from merge requests index page
+  - Ensure validation messages are shown within the milestone form
+  - Add notification_settings API calls !5632 (mahcsig)
+  - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
+  - Fix URLs with anchors in wiki !6300 (houqp)
+  - Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska)
+  - Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225
+  - Fix Gitlab::Popen.popen thread-safety issue
+  - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
+  - Clean environment variables when running git hooks
+  - Fix Import/Export issues importing protected branches and some specific models
+  - Fix non-master branch readme display in tree view
+  - Add UX improvements for merge request version diffs
+
+## 8.11.11 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
+## 8.11.10 (2016-11-02)
+
+- Removes any symlinks before importing a project export file. CVE-2016-9086
+
+## 8.11.9
+
+  - Don't send Private-Token (API authentication) headers to Sentry
+  - Share projects via the API only with groups the authenticated user can access
+
+## 8.11.8
+
+  - Respect the fork_project permission when forking projects
+  - Set a restrictive CORS policy on the API for credentialed requests
+  - API: disable rails session auth for non-GET/HEAD requests
+  - Escape HTML nodes in builds commands in CI linter
+
+## 8.11.7
+
+  - Avoid conflict with admin labels when importing GitHub labels. !6158
+  - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
+  - Allow the Rails cookie to be used for API authentication.
+  - Login/Register UX upgrade !6328
+
+## 8.11.6
+
+  - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
+  - Make merge conflict file size limit 200 KB, to match the docs. !6052
+  - Fix an error where we were unable to create a CommitStatus for running state. !6107
+  - Optimize discussion notes resolving and unresolving. !6141
+  - Fix GitLab import button. !6167
+  - Restore SSH Key title auto-population behavior. !6186
+  - Fix DB schema to match latest migration. !6256
+  - Exclude some pending or inactivated rows in Member scopes.
+
+## 8.11.5
+
+  - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087
+  - Fix member expiration date picker after update. !6184
+  - Fix suggested colors options for new labels in the admin area. !6138
+  - Optimize discussion notes resolving and unresolving
+  - Fix GitLab import button
+  - Fix confidential issues being exposed as public using gitlab.com export
+  - Remove gitorious from import_sources. !6180
+  - Scope webhooks/services that will run for confidential issues
+  - Remove gitorious from import_sources
+  - Fix confidential issues being exposed as public using gitlab.com export
+  - Use oj gem for faster JSON processing
+
+## 8.11.4
+
+  - Fix resolving conflicts on forks. !6082
+  - Fix diff commenting on merge requests created prior to 8.10. !6029
+  - Fix pipelines tab layout regression. !5952
+  - Fix "Wiki" link not appearing in navigation for projects with external wiki. !6057
+  - Do not enforce using hash with hidden key in CI configuration. !6079
+  - Fix hover leading space bug in pipeline graph !5980
+  - Fix sorting issues by "last updated" doesn't work after import from GitHub
+  - GitHub importer use default project visibility for non-private projects
+  - Creating an issue through our API now emails label subscribers !5720
+  - Block concurrent updates for Pipeline
+  - Don't create groups for unallowed users when importing projects
+  - Fix issue boards leak private label names and descriptions
+  - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
+  - Remove gitorious. !5866
+  - Allow compare merge request versions
+
+## 8.11.3
+
+  - Allow system info page to handle case where info is unavailable
+  - Label list shows all issues (opened or closed) with that label
+  - Don't show resolve conflicts link before MR status is updated
+  - Fix IE11 fork button bug !5982
+  - Don't prevent viewing the MR when git refs for conflicts can't be found on disk
+  - Fix external issue tracker "Issues" link leading to 404s
+  - Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
+  - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+  - Issues filters reset button
+
+## 8.11.2
+
+  - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
+  - Use gitlab-workhorse 0.7.11 !5983
+  - Does not halt the GitHub import process when an error occurs. !5763
+  - Fix file links on project page when default view is Files !5933
+  - Fixed enter key in search input not working !5888
+
+## 8.11.1
+
+  - Pulled due to packaging error.
+
+## 8.11.0 (2016-08-22)
+
+  - Use test coverage value from the latest successful pipeline in badge. !5862
   - Add test coverage report badge. !5708
   - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
+  - Add Koding (online IDE) integration
   - Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
   - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
   - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
+  - Fix adding line comments on the initial commit to a repo !5900
   - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
   - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
   - Update to Ruby 2.3.1. !4948
+  - Add Issues Board !5548
+  - Allow resolving merge conflicts in the UI !5479
   - Improve diff performance by eliminating redundant checks for text blobs
   - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
   - Convert switch icon into icon font (ClemMakesApps)
   - API: Endpoints for enabling and disabling deploy keys
   - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
   - Use long options for curl examples in documentation !5703 (winniehell)
+  - Added tooltip listing label names to the labels value in the collapsed issuable sidebar
   - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
+  - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
   - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
+  - Allow naming U2F devices !5833
   - Ignore URLs starting with // in Markdown links !5677 (winniehell)
   - Fix CI status icon link underline (ClemMakesApps)
   - The Repository class is now instrumented
+  - Fix commit mention font inconsistency (ClemMakesApps)
+  - Do not escape URI when extracting path !5878 (winniehell)
   - Fix filter label tooltip HTML rendering (ClemMakesApps)
   - Cache the commit author in RequestStore to avoid extra lookups in PostReceive
   - Expand commit message width in repo view (ClemMakesApps)
   - Cache highlighted diff lines for merge requests
   - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
+  - Allow merge request diff notes and discussions to be explicitly marked as resolved
+  - API: Add deployment endpoints
+  - API: Add Play endpoint on Builds
   - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+  - Show wall clock time when showing a pipeline. !5734
   - Show member roles to all users on members page
   - Project.visible_to_user is instrumented again
   - Fix awardable button mutuality loading spinners (ClemMakesApps)
+  - Sort todos by date and priority
   - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
   - Optimize maximum user access level lookup in loading of notes
+  - Send notification emails to users newly mentioned in issue and MR edits !5800
   - Add "No one can push" as an option for protected branches. !5081
   - Improve performance of AutolinkFilter#text_parse by using XPath
   - Add experimental Redis Sentinel support !1877
@@ -41,13 +743,15 @@ v 8.11.0 (unreleased)
   - Update `timeago` plugin to use multiple string/locale settings
   - Remove unused images (ClemMakesApps)
   - Get issue and merge request description templates from repositories
-  - Add hover state to todos !5361 (winniehell)
+  - Enforce 2FA restrictions on API authentication endpoints !5820
   - Limit git rev-list output count to one in forced push check
   - Show deployment status on merge requests with external URLs
   - Clean up unused routes (Josef Strzibny)
   - Fix issue on empty project to allow developers to only push to protected branches if given permission
+  - API: Add enpoints for pipelines
   - Add green outline to New Branch button. !5447 (winniehell)
   - Optimize generating of cache keys for issues and notes
+  - Fix repository push email formatting in Outlook
   - Improve performance of syntax highlighting Markdown code blocks
   - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
   - Remove delay when hitting "Reply..." button on page with a lot of discussions
@@ -56,14 +760,16 @@ v 8.11.0 (unreleased)
   - Upgrade Grape from 0.13.0 to 0.15.0. !4601
   - Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries
   - Fix devise deprecation warnings.
+  - Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
   - Update version_sorter and use new interface for faster tag sorting
   - Optimize checking if a user has read access to a list of issues !5370
   - Store all DB secrets in secrets.yml, under descriptive names !5274
+  - Fix syntax highlighting in file editor
+  - Support slash commands in issue and merge request descriptions as well as comments. !5021
   - Nokogiri's various parsing methods are now instrumented
   - Add archived badge to project list !5798
   - Add simple identifier to public SSH keys (muteor)
   - Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
-  - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
   - Fix filter input alignment (ClemMakesApps)
   - Include old revision in merge request update hooks (Ben Boeckel)
   - Add build event color in HipChat messages (David Eisner)
@@ -90,56 +796,106 @@ v 8.11.0 (unreleased)
   - Allow branch names ending with .json for graph and network page !5579 (winniehell)
   - Add the `sprockets-es6` gem
   - Improve OAuth2 client documentation (muteor)
+  - Fix diff comments inverted toggle bug (ClemMakesApps)
   - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
   - Profile requests when a header is passed
   - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
   - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
   - Add commit stats in commit api. !5517 (dixpac)
   - Add CI configuration button on project page
+  - Fix merge request new view not changing code view rendering style
+  - edit_blob_link will use blob passed onto the options parameter
   - Make error pages responsive (Takuya Noguchi)
   - The performance of the project dropdown used for moving issues has been improved
   - Fix skip_repo parameter being ignored when destroying a namespace
+  - Add all builds into stage/job dropdowns on builds page
   - Change requests_profiles resource constraint to catch virtually any file
   - Bump gitlab_git to lazy load compare commits
   - Reduce number of queries made for merge_requests/:id/diffs
+  - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
   - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
   - Fix bug where destroying a namespace would not always destroy projects
   - Fix RequestProfiler::Middleware error when code is reloaded in development
+  - Allow horizontal scrolling of code blocks in issue body
   - Catch what warden might throw when profiling requests to re-throw it
   - Avoid commit lookup on diff_helper passing existing local variable to the helper method
   - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac)
   - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
   - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
   - Adds support for pending invitation project members importing projects
+  - Add pipeline visualization/graph on pipeline page
   - Update devise initializer to turn on changed password notification emails. !5648 (tombell)
   - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
   - Fix importing GitLab projects with an invalid MR source project
   - Sort folders with submodules in Files view !5521
   - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
   - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
+  - Add pipelines tab to merge requests
+  - Fix notification_service argument error of declined invitation emails
   - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
   - Speed up todos queries by limiting the projects set we join with
   - Ensure file editing in UI does not overwrite commited changes without warning user
+  - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
+  - Update gitlab_git gem to 10.4.7
+  - Simplify SQL queries of marking a todo as done
+
+## 8.10.13 (2016-11-02)
+
+- Removes any symlinks before importing a project export file. CVE-2016-9086
+
+## 8.10.12
+
+  - Don't send Private-Token (API authentication) headers to Sentry
+  - Share projects via the API only with groups the authenticated user can access
+
+## 8.10.11
+
+  - Respect the fork_project permission when forking projects
+  - Set a restrictive CORS policy on the API for credentialed requests
+  - API: disable rails session auth for non-GET/HEAD requests
+  - Escape HTML nodes in builds commands in CI linter
+
+## 8.10.10
+
+  - Allow the Rails cookie to be used for API authentication.
+
+## 8.10.9
+
+  - Exclude some pending or inactivated rows in Member scopes
+
+## 8.10.8
+
+  - Fix information disclosure in issue boards.
+  - Fix privilege escalation in project import.
+
+## 8.10.7
+
+  - Upgrade Hamlit to 2.6.1. !5873
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+## 8.10.6
 
-v 8.10.6
   - Upgrade Rails to 4.2.7.1 for security fixes. !5781
   - Restore "Largest repository" sort option on Admin > Projects page. !5797
   - Fix privilege escalation via project export.
   - Require administrator privileges to perform a project import.
   - Allow to add deploy keys with write-access !5807 (Ali Ibrahim)
 
-v 8.10.5
+## 8.10.5
+
   - Add a data migration to fix some missing timestamps in the members table. !5670
   - Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706
   - Cache project count for 5 minutes to reduce DB load. !5746 & !5754
 
-v 8.10.4
+## 8.10.4
+
   - Don't close referenced upstream issues from a forked project.
   - Fixes issue with dropdowns `enter` key not working correctly. !5544
   - Fix Import/Export project import not working in HA mode. !5618
   - Fix Import/Export error checking versions. !5638
 
-v 8.10.3
+## 8.10.3
+
   - Fix Import/Export issue importing milestones and labels not associated properly. !5426
   - Fix timing problems running imports on production. !5523
   - Add a log message when a project is scheduled for destruction for debugging. !5540
@@ -150,9 +906,8 @@ v 8.10.3
   - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
   - Fix label already exist error message in the right sidebar.
 
-v 8.10.3 (unreleased)
+## 8.10.2
 
-v 8.10.2
   - User can now search branches by name. !5144
   - Page is now properly rendered after committing the first file and creating the first branch. !5399
   - Add branch or tag icon to ref in builds page. !5434
@@ -173,7 +928,8 @@ v 8.10.2
   - Fix missing schema update for `20160722221922`. !5512
   - Update `gitlab-shell` version to 3.2.1 in the 8.9->8.10 update guide. !5516
 
-v 8.10.1
+## 8.10.1
+
   - Refactor repository storages documentation. !5428
   - Gracefully handle case when keep-around references are corrupted or exist already. !5430
   - Add detailed info on storage path mountpoints. !5437
@@ -182,7 +938,8 @@ v 8.10.1
   - Ignore invalid trusted proxies in X-Forwarded-For header. !5454
   - Add links to the real markdown.md file for all GFM examples. !5458
 
-v 8.10.0
+## 8.10.0 (2016-07-22)
+
   - Fix profile activity heatmap to show correct day name (eanplatter)
   - Speed up ExternalWikiHelper#get_project_wiki_path
   - Expose {should,force}_remove_source_branch (Ben Boeckel)
@@ -344,12 +1101,34 @@ v 8.10.0
   - Export and import avatar as part of project import/export
   - Fix migration corrupting import data for old version upgrades
   - Show tooltip on GitLab export link in new project page
+  - Fix import_data wrongly saved as a result of an invalid import_url !5206
+
+## 8.9.11
+
+  - Respect the fork_project permission when forking projects
+  - Set a restrictive CORS policy on the API for credentialed requests
+  - API: disable rails session auth for non-GET/HEAD requests
+  - Escape HTML nodes in builds commands in CI linter
+
+## 8.9.10
+
+  - Allow the Rails cookie to be used for API authentication.
+
+## 8.9.9
+
+  - Exclude some pending or inactivated rows in Member scopes
+
+## 8.9.8
+
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+## 8.9.7
 
-v 8.9.7
   - Upgrade Rails to 4.2.7.1 for security fixes. !5781
   - Require administrator privileges to perform a project import.
 
-v 8.9.6
+## 8.9.6
+
   - Fix importing of events under notes for GitLab projects. !5154
   - Fix log statements in import/export. !5129
   - Fix commit avatar alignment in compare view. !5128
@@ -358,13 +1137,8 @@ v 8.9.6
   - Keeps issue number when importing from Gitlab.com
   - Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
 
-v 8.9.7 (unreleased)
-  - Fix import_data wrongly saved as a result of an invalid import_url
-
-v 8.9.6
-  - Fix importing of events under notes for GitLab projects
+## 8.9.5
 
-v 8.9.5
   - Add more debug info to import/export and memory killer. !5108
   - Fixed avatar alignment in new MR view. !5095
   - Fix diff comments not showing up in activity feed. !5069
@@ -379,7 +1153,8 @@ v 8.9.5
   - Update RedCloth to 4.3.2 for CVE-2012-6684. !4929 (Takuya Noguchi)
   - Improve the request / withdraw access button. !4860
 
-v 8.9.4
+## 8.9.4
+
   - Fix privilege escalation issue with OAuth external users.
   - Ensure references to private repos aren't shown to logged-out users.
   - Fixed search field blur not removing focus. !4704
@@ -393,7 +1168,8 @@ v 8.9.4
   - Expiry date on pinned nav cookie. !5009
   - Updated breakpoint for sidebar pinning. !5019
 
-v 8.9.3
+## 8.9.3
+
   - Fix encrypted data backwards compatibility after upgrading attr_encrypted gem. !4963
   - Fix rendering of commit notes. !4953
   - Resolve "Pin should show up at 1280px min". !4947
@@ -410,12 +1186,14 @@ v 8.9.3
   - Use update_columns to bypass all the dirty code on active_record. !4985
   - Fix restore Rake task warning message output !4980
 
-v 8.9.2
+## 8.9.2
+
   - Fix visibility of snippets when searching.
   - Fix an information disclosure when requesting access to a group containing private projects.
   - Update omniauth-saml to 1.6.0 !4951
 
-v 8.9.1
+## 8.9.1
+
   - Refactor labels documentation. !3347
   - Eager load award emoji on notes. !4628
   - Fix some CI wording in documentation. !4660
@@ -459,7 +1237,8 @@ v 8.9.1
   - Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915
   - Remove duplicate 'New Page' button on edit wiki page
 
-v 8.9.0
+## 8.9.0 (2016-06-22)
+
   - Fix group visibility form layout in application settings
   - Fix builds API response not including commit data
   - Fix error when CI job variables key specified but not defined
@@ -614,18 +1393,26 @@ v 8.9.0
   - Add tooltip to pin/unpin navbar
   - Add new sub nav style to Wiki and Graphs sub navigation
 
-v 8.8.8
+## 8.8.9
+
+  - Upgrade Doorkeeper to 4.2.0. !5881
+
+## 8.8.8
+
   - Upgrade Rails to 4.2.7.1 for security fixes. !5781
 
-v 8.8.7
+## 8.8.7
+
   - Fix privilege escalation issue with OAuth external users.
   - Ensure references to private repos aren't shown to logged-out users.
 
-v 8.8.6
+## 8.8.6
+
   - Fix visibility of snippets when searching.
   - Update omniauth-saml to 1.6.0 !4951
 
-v 8.8.5
+## 8.8.5
+
   - Import GitHub repositories respecting the API rate limit !4166
   - Fix todos page throwing errors when you have a project pending deletion !4300
   - Disable Webhooks before proceeding with the GitHub import !4470
@@ -638,12 +1425,14 @@ v 8.8.5
   - Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
   - Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
 
-v 8.8.4
+## 8.8.4
+
   - Fix LDAP-based login for users with 2FA enabled. !4493
   - Added descriptions to notification settings dropdown
   - Due date can be removed from milestones
 
-v 8.8.3
+## 8.8.3
+
   - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
   - Fixed JS error when trying to remove discussion form. !4303
   - Fixed issue with button color when no CI enabled. !4287
@@ -662,7 +1451,8 @@ v 8.8.3
   - Fix missing number on generated ordered list element. !4437
   - Prevent disclosure of notes on confidential issues in search results.
 
-v 8.8.2
+## 8.8.2
+
   - Added remove due date button. !4209
   - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources. !4242
   - Fix Error 500 in CI charts by gracefully handling commits with no durations. !4245
@@ -673,13 +1463,15 @@ v 8.8.2
   - When creating a .gitignore file a dropdown with templates will be provided. !4075
   - Fix concurrent request when updating build log in browser. !4183
 
-v 8.8.1
+## 8.8.1
+
   - Add documentation for the "Health Check" feature
   - Allow anonymous users to access a public project's pipelines !4233
   - Fix MySQL compatibility in zero downtime migrations helpers
   - Fix the CI login to Container Registry (the gitlab-ci-token user)
 
-v 8.8.0
+## 8.8.0 (2016-05-22)
+
   - Implement GFM references for milestones (Alejandro Rodríguez)
   - Snippets tab under user profile. !4001 (Long Nguyen)
   - Fix error when using link to uploads in global snippets
@@ -755,34 +1547,40 @@ v 8.8.0
   - When creating a .gitignore file a dropdown with templates will be provided
   - Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562
 
-v 8.7.9
+## 8.7.9
+
   - Fix privilege escalation issue with OAuth external users.
   - Ensure references to private repos aren't shown to logged-out users.
 
-v 8.7.8
+## 8.7.8
+
   - Fix visibility of snippets when searching.
   - Update omniauth-saml to 1.6.0 !4951
 
-v 8.7.7
+## 8.7.7
+
   - Fix import by `Any Git URL` broken if the URL contains a space
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
   - Only show notes through JSON on confidential issues that the user has access to
 
-v 8.7.6
+## 8.7.6
+
   - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
   - Fix import from GitLab.com to a private instance failure. !4181
   - Fix external imports not finding the import data. !4106
   - Fix notification delay when changing status of an issue
   - Bump Workhorse to 0.7.5 so it can serve raw diffs
 
-v 8.7.5
+## 8.7.5
+
   - Fix relative links in wiki pages. !4050
   - Fix always showing build notification message when switching between merge requests !4086
   - Fix an issue when filtering merge requests with more than one label. !3886
   - Fix short note for the default scope on build page (Takuya Noguchi)
 
-v 8.7.4
+## 8.7.4
+
   - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss)
   - Fix setting trusted proxies !3970
   - Fix BitBucket importer bug when throwing exceptions !3941
@@ -791,20 +1589,23 @@ v 8.7.4
   - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100
   - Use a case-insensitive comparison in sanitizing URI schemes
 
-v 8.7.3
+## 8.7.3
+
   - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented
   - Merge request widget displays TeamCity build state and code coverage correctly again.
   - Fix the line code when importing PR review comments from GitHub. !4010
   - Wikis are now initialized on legacy projects when checking repositories
   - Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea)
 
-v 8.7.2
+## 8.7.2
+
   - The "New Branch" button is now loaded asynchronously
   - Fix error 500 when trying to create a wiki page
   - Updated spacing between notification label and button
   - Label titles in filters are now escaped properly
 
-v 8.7.1
+## 8.7.1
+
   - Throttle the update of `project.last_activity_at` to 1 minute. !3848
   - Fix .gitlab-ci.yml parsing issue when hidde job is a template without script definition. !3849
   - Fix license detection to detect all license files, not only known licenses. !3878
@@ -814,7 +1615,8 @@ v 8.7.1
   - Update width of search box to fix Safari bug. !3900 (Jedidiah)
   - Use the `can?` helper instead of `current_user.can?`
 
-v 8.7.0
+## 8.7.0 (2016-04-22)
+
   - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented
   - Fix vulnerability that made it possible to gain access to private labels and milestones
   - The number of InfluxDB points stored per UDP packet can now be configured
@@ -930,12 +1732,14 @@ v 8.7.0
   - Add RAW build trace output and button on build page
   - Add incremental build trace update into CI API
 
-v 8.6.9
+## 8.6.9
+
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
   - Only show notes through JSON on confidential issues that the user has access to
 
-v 8.6.8
+## 8.6.8
+
   - Prevent privilege escalation via "impersonate" feature
   - Prevent privilege escalation via notes API
   - Prevent privilege escalation via project webhook API
@@ -948,12 +1752,14 @@ v 8.6.8
   - Prevent information disclosure via project labels
   - Prevent information disclosure via new merge request page
 
-v 8.6.7
+## 8.6.7
+
   - Fix persistent XSS vulnerability in `commit_person_link` helper
   - Fix persistent XSS vulnerability in Label and Milestone dropdowns
   - Fix vulnerability that made it possible to enumerate private projects belonging to group
 
-v 8.6.6
+## 8.6.6
+
   - Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
   - Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
   - Fix revoking of authorized OAuth applications (Connor Shea). !3690
@@ -961,7 +1767,8 @@ v 8.6.6
   - Issuable header is consistent between issues and merge requests
   - Improved spacing in issuable header on mobile
 
-v 8.6.5
+## 8.6.5
+
   - Fix importing from GitHub Enterprise. !3529
   - Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533
   - Check permissions when user attempts to import members from another project. !3535
@@ -970,11 +1777,13 @@ v 8.6.5
   - Unblock user when active_directory is disabled and it can be found !3550
   - Fix a 2FA authentication spoofing vulnerability.
 
-v 8.6.4
+## 8.6.4
+
   - Don't attempt to fetch any tags from a forked repo (Stan Hu)
   - Redesign the Labels page
 
-v 8.6.3
+## 8.6.3
+
   - Mentions on confidential issues doesn't create todos for non-members. !3374
   - Destroy related todos when an Issue/MR is deleted. !3376
   - Fix error 500 when target is nil on todo list. !3376
@@ -987,7 +1796,8 @@ v 8.6.3
   - Fix issue with dropdowns not selecting values. !3478
   - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
 
-v 8.6.2
+## 8.6.2
+
   - Fix dropdown alignment. !3298
   - Fix issuable sidebar overlaps on tablet. !3299
   - Make dropdowns pixel perfect. !3337
@@ -1009,7 +1819,8 @@ v 8.6.2
   - Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
   - Fixed issue with notification settings not saving. !3452
 
-v 8.6.1
+## 8.6.1
+
   - Add option to reload the schema before restoring a database backup. !2807
   - Display navigation controls on mobile. !3214
   - Fixed bug where participants would not work correctly on merge requests. !3329
@@ -1024,7 +1835,8 @@ v 8.6.1
   - Fixes issue with assign milestone not loading milestone list. !3346
   - Fix an issue causing the Dashboard/Milestones page to be blank. !3348
 
-v 8.6.0
+## 8.6.0 (2016-03-22)
+
   - Add ability to move issue to another project
   - Prevent tokens in the import URL to be showed by the UI
   - Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
@@ -1089,11 +1901,13 @@ v 8.6.0
   - Trigger a todo for mentions on commits page
   - Let project owners and admins soft delete issues and merge requests
 
-v 8.5.13
+## 8.5.13
+
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
 
-v 8.5.12
+## 8.5.12
+
   - Prevent privilege escalation via "impersonate" feature
   - Prevent privilege escalation via notes API
   - Prevent privilege escalation via project webhook API
@@ -1104,41 +1918,51 @@ v 8.5.12
   - Prevent information disclosure via project labels
   - Prevent information disclosure via new merge request page
 
-v 8.5.11
+## 8.5.11
+
   - Fix persistent XSS vulnerability in `commit_person_link` helper
 
-v 8.5.10
+## 8.5.10
+
   - Fix a 2FA authentication spoofing vulnerability.
 
-v 8.5.9
+## 8.5.9
+
   - Don't attempt to fetch any tags from a forked repo (Stan Hu).
 
-v 8.5.8
+## 8.5.8
+
   - Bump Git version requirement to 2.7.4
 
-v 8.5.7
+## 8.5.7
+
   - Bump Git version requirement to 2.7.3
 
-v 8.5.6
+## 8.5.6
+
   - Obtain a lease before querying LDAP
 
-v 8.5.5
+## 8.5.5
+
   - Ensure removing a project removes associated Todo entries
   - Prevent a 500 error in Todos when author was removed
   - Fix pagination for filtered dashboard and explore pages
   - Fix "Show all" link behavior
 
-v 8.5.4
+## 8.5.4
+
   - Do not cache requests for badges (including builds badge)
 
-v 8.5.3
+## 8.5.3
+
   - Flush repository caches before renaming projects
   - Sort starred projects on dashboard based on last activity by default
   - Show commit message in JIRA mention comment
   - Makes issue page and merge request page usable on mobile browsers.
   - Improved UI for profile settings
 
-v 8.5.2
+## 8.5.2
+
   - Fix sidebar overlapping content when screen width was below 1200px
   - Don't repeat labels listed on Labels tab
   - Bring the "branded appearance" feature from EE to CE
@@ -1155,7 +1979,8 @@ v 8.5.2
   - Don't show "Welcome to GitLab" when the search didn't return any projects
   - Add Todos documentation
 
-v 8.5.1
+## 8.5.1
+
   - Fix group projects styles
   - Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec)
   - Fix a set of small UI glitches in project, profile, and wiki pages
@@ -1175,7 +2000,8 @@ v 8.5.1
   - Add build coverage in project's builds page (Steffen Köhler)
   - Changed # to ! for merge requests in activity view
 
-v 8.5.0
+## 8.5.0 (2016-02-22)
+
   - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
   - Cache various Repository methods to improve performance
   - Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
@@ -1254,11 +2080,13 @@ v 8.5.0
   - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
   - Add Todos
 
-v 8.4.11
+## 8.4.11
+
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
 
-v 8.4.10
+## 8.4.10
+
   - Prevent privilege escalation via "impersonate" feature
   - Prevent privilege escalation via notes API
   - Prevent privilege escalation via project webhook API
@@ -1269,28 +2097,35 @@ v 8.4.10
   - Prevent information disclosure via project labels
   - Prevent information disclosure via new merge request page
 
-v 8.4.9
+## 8.4.9
+
   - Fix persistent XSS vulnerability in `commit_person_link` helper
 
-v 8.4.8
+## 8.4.8
+
   - Fix a 2FA authentication spoofing vulnerability.
 
-v 8.4.7
+## 8.4.7
+
   - Don't attempt to fetch any tags from a forked repo (Stan Hu).
 
-v 8.4.6
+## 8.4.6
+
   - Bump Git version requirement to 2.7.4
 
-v 8.4.5
+## 8.4.5
+
   - No CE-specific changes
 
-v 8.4.4
+## 8.4.4
+
   - Update omniauth-saml gem to 1.4.2
   - Prevent long-running backup tasks from timing out the database connection
   - Add a Project setting to allow guests to view build logs (defaults to true)
   - Sort project milestones by due date including issue editor (Oliver Rogers / Orih)
 
-v 8.4.3
+## 8.4.3
+
   - Increase lfs_objects size column to 8-byte integer to allow files larger
     than 2.1GB
   - Correctly highlight MR diff when MR has merge conflicts
@@ -1301,7 +2136,8 @@ v 8.4.3
     performance monitoring
   - Allow autosize textareas to also be manually resized
 
-v 8.4.2
+## 8.4.2
+
   - Bump required gitlab-workhorse version to bring in a fix for missing
     artifacts in the build artifacts browser
   - Get rid of those ugly borders on the file tree view
@@ -1314,14 +2150,16 @@ v 8.4.2
   - Fix method undefined when using external commit status in builds
   - Fix highlighting in blame view.
 
-v 8.4.1
+## 8.4.1
+
   - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3),
     and Nokogiri (1.6.7.2)
   - Fix redirect loop during import
   - Fix diff highlighting for all syntax themes
   - Delete project and associations in a background worker
 
-v 8.4.0
+## 8.4.0 (2016-01-22)
+
   - Allow LDAP users to change their email if it was not set by the LDAP server
   - Ensure Gravatar host looks like an actual host
   - Consider re-assign as a mention from a notification point of view
@@ -1394,11 +2232,13 @@ v 8.4.0
   - Add IP check against DNSBLs at account sign-up
   - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
 
-v 8.3.10
+## 8.3.10
+
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
 
-v 8.3.9
+## 8.3.9
+
   - Prevent privilege escalation via "impersonate" feature
   - Prevent privilege escalation via notes API
   - Prevent privilege escalation via project webhook API
@@ -1407,22 +2247,28 @@ v 8.3.9
   - Prevent information disclosure via project labels
   - Prevent information disclosure via new merge request page
 
-v 8.3.8
+## 8.3.8
+
   - Fix persistent XSS vulnerability in `commit_person_link` helper
 
-v 8.3.7
+## 8.3.7
+
   - Fix a 2FA authentication spoofing vulnerability.
 
-v 8.3.6
+## 8.3.6
+
   - Don't attempt to fetch any tags from a forked repo (Stan Hu).
 
-v 8.3.5
+## 8.3.5
+
   - Bump Git version requirement to 2.7.4
 
-v 8.3.4
+## 8.3.4
+
   - Use gitlab-workhorse 0.5.4 (fixes API routing bug)
 
-v 8.3.3
+## 8.3.3
+
   - Preserve CE behavior with JIRA integration by only calling API if URL is set
   - Fix duplicated branch creation/deletion events when using Web UI (Stan Hu)
   - Add configurable LDAP server query timeout
@@ -1438,17 +2284,20 @@ v 8.3.3
   - Fix: maintain milestone filter between Open and Closed tabs (Greg Smethells)
   - Fix missing artifacts and build traces for build created before 8.3
 
-v 8.3.2
+## 8.3.2
+
   - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
   - Add support for Google reCAPTCHA in user registration
 
-v 8.3.1
+## 8.3.1
+
   - Fix Error 500 when global milestones have slashes (Stan Hu)
   - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
   - Fix LDAP identity and user retrieval when special characters are used
   - Move Sidekiq-cron configuration to gitlab.yml
 
-v 8.3.0
+## 8.3.0 (2015-12-22)
+
   - Bump rack-attack to 4.3.1 for security fix (Stan Hu)
   - API support for starred projects for authorized user (Zeger-Jan van de Weg)
   - Add open_issues_count to project API (Stan Hu)
@@ -1516,11 +2365,13 @@ v 8.3.0
   - Expose Git's version in the admin area
   - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
 
-v 8.2.6
+## 8.2.6
+
   - Prevent unauthorized access to other projects build traces
   - Forbid scripting for wiki files
 
-v 8.2.5
+## 8.2.5
+
   - Prevent privilege escalation via "impersonate" feature
   - Prevent privilege escalation via notes API
   - Prevent privilege escalation via project webhook API
@@ -1528,10 +2379,12 @@ v 8.2.5
   - Prevent information disclosure via project labels
   - Prevent information disclosure via new merge request page
 
-v 8.2.4
+## 8.2.4
+
   - Bump Git version requirement to 2.7.4
 
-v 8.2.3
+## 8.2.3
+
   - Fix application settings cache not expiring after changes (Stan Hu)
   - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
   - Update documentation for "Guest" permissions
@@ -1540,7 +2393,8 @@ v 8.2.3
   - Webhook payload has an added, modified and removed properties for each commit
   - Fix 500 error when creating a merge request that removes a submodule
 
-v 8.2.2
+## 8.2.2
+
   - Fix 404 in redirection after removing a project (Stan Hu)
   - Ensure cached application settings are refreshed at startup (Stan Hu)
   - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
@@ -1550,11 +2404,13 @@ v 8.2.2
   - Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
   - Fix: duplicate email notifications on issue comments
 
-v 8.2.1
+## 8.2.1
+
   - Forcefully update builds that didn't want to update with state machine
   - Fix: saving GitLabCiService as Admin Template
 
-v 8.2.0
+## 8.2.0 (2015-11-22)
+
   - Improved performance of finding projects and groups in various places
   - Improved performance of rendering user profile pages and Atom feeds
   - Expose build artifacts path as config option
@@ -1614,19 +2470,22 @@ v 8.2.0
   - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
   - Add Award Emoji to issue and merge request pages
 
-v 8.1.4
+## 8.1.4
+
   - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
   - Prevent redirect loop when home_page_url is set to the root URL
   - Fix incoming email config defaults
   - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
 
-v 8.1.3
+## 8.1.3
+
   - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu)
   - Spread out runner contacted_at updates
   - Use issue editor as cross reference comment author when issue is edited with a new mention
   - Add Facebook authentication
 
-v 8.1.1
+## 8.1.2
+
   - Fix cloning Wiki repositories via HTTP (Stan Hu)
   - Add migration to remove satellites directory
   - Fix specific runners visibility
@@ -1636,10 +2495,12 @@ v 8.1.1
   - Fix CI badge
   - Allow developer to manage builds
 
-v 8.1.1
+## 8.1.1
+
   - Removed, see 8.1.2
 
-v 8.1.0
+## 8.1.0 (2015-10-22)
+
   - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
   - Fix duplicate repositories in GitHub import page (Stan Hu)
   - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
@@ -1724,11 +2585,13 @@ v 8.1.0
   - Fix padding of outdated discussion item.
   - Animate the logo on hover
 
-v 8.0.5
+## 8.0.5
+
   - Correct lookup-by-email for LDAP logins
   - Fix loading spinner sometimes not being hidden on Merge Request tab switches
 
-v 8.0.4
+## 8.0.4
+
   - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
   - Fix referrals for :back and relative URL installs
   - Fix anchors to comments in diffs
@@ -1737,13 +2600,15 @@ v 8.0.4
   - Fix search in Files
   - Add full project namespace to payload of system webhooks (Ricardo Band)
 
-v 8.0.3
+## 8.0.3
+
   - Fix URL shown in Slack notifications
   - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
   - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
   - Add work_in_progress key to MR webhooks (Ben Boeckel)
 
-v 8.0.2
+## 8.0.2
+
   - Fix default avatar not rendering in network graph (Stan Hu)
   - Skip check_initd_configured_correctly on omnibus installs
   - Prevent double-prefixing of help page paths
@@ -1757,10 +2622,12 @@ v 8.0.2
   - Add option to use StartTLS with Reply by email IMAP server.
   - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie)
 
-v 8.0.1
+## 8.0.1
+
   - Improve CI migration procedure and documentation
 
-v 8.0.0
+## 8.0.0 (2015-09-22)
+
   - Fix Markdown links not showing up in dashboard activity feed (Stan Hu)
   - Remove milestones from merge requests when milestones are deleted (Stan Hu)
   - Fix HTML link that was improperly escaped in new user e-mail (Stan Hu)
@@ -1825,1692 +2692,6 @@ v 8.0.0
   - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato)
   - Removed API calls from CE to CI
 
-v 7.14.3
-  - No changes
-
-v 7.14.2
-  - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
-  - Allow configuration of LDAP attributes GitLab will use for the new user account.
-
-v 7.14.1
-  - Improve abuse reports management from admin area
-  - Fix "Reload with full diff" URL button in compare branch view (Stan Hu)
-  - Disabled DNS lookups for SSH in docker image (Rowan Wookey)
-  - Only include base URL in OmniAuth full_host parameter (Stan Hu)
-  - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
-  - Ability to enable SSL verification for Webhooks
-
-v 7.14.0
-  - Fix bug where non-project members of the target project could set labels on new merge requests.
-  - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller)
-  - Fix redirection after sign in when using auto_sign_in_with_provider
-  - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu)
-  - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu)
-  - Provide more feedback what went wrong if HipChat service failed test (Stan Hu)
-  - Fix bug where backslashes in inline diffs could be dropped (Stan Hu)
-  - Disable turbolinks when linking to Bitbucket import status (Stan Hu)
-  - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu)
-  - Fix corrupted binary files when using API files endpoint (Stan Hu)
-  - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu)
-  - Show incompatible projects in Bitbucket import status (Stan Hu)
-  - Fix coloring of diffs on MR Discussion-tab (Gert Goet)
-  - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu)
-  - Fix errors deleting and creating branches with encoded slashes (Stan Hu)
-  - Always add current user to autocomplete controller to support filter by "Me" (Stan Hu)
-  - Fix multi-line syntax highlighting (Stan Hu)
-  - Fix network graph when branch name has single quotes (Stan Hu)
-  - Add "Confirm user" button in user admin page (Stan Hu)
-  - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu)
-  - Add support for Unicode filenames in relative links (Hiroyuki Sato)
-  - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki)
-  - Fix commit data retrieval when branch name has single quotes (Stan Hu)
-  - Check that project was actually created rather than just validated in import:repos task (Stan Hu)
-  - Fix full screen mode for snippet comments (Daniel Gerhardt)
-  - Fix 404 error in files view after deleting the last file in a repository (Stan Hu)
-  - Fix the "Reload with full diff" URL button (Stan Hu)
-  - Fix label read access for unauthenticated users (Daniel Gerhardt)
-  - Fix access to disabled features for unauthenticated users (Daniel Gerhardt)
-  - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu)
-  - Fix file upload dialog for comment editing (Daniel Gerhardt)
-  - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu)
-  - Return comments in created order in merge request API (Stan Hu)
-  - Disable internal issue tracker controller if external tracker is used (Stan Hu)
-  - Expire Rails cache entries after two weeks to prevent endless Redis growth
-  - Add support for destroying project milestones (Stan Hu)
-  - Allow custom backup archive permissions
-  - Add project star and fork count, group avatar URL and user/group web URL attributes to API
-  - Show who last edited a comment if it wasn't the original author
-  - Send notification to all participants when MR is merged.
-  - Add ability to manage user email addresses via the API.
-  - Show buttons to add license, changelog and contribution guide if they're missing.
-  - Tweak project page buttons.
-  - Disabled autocapitalize and autocorrect on login field (Daryl Chan)
-  - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis)
-  - Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller)
-  - Remove redis-store TTL monkey patch
-  - Add support for CI skipped status
-  - Fetch code from forks to refs/merge-requests/:id/head when merge request created
-  - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg)
-  - Add "Check out branch" button to the MR page.
-  - Improve MR merge widget text and UI consistency.
-  - Improve text in MR "How To Merge" modal.
-  - Cache all events
-  - Order commits by date when comparing branches
-  - Fix bug causing error when the target branch of a symbolic ref was deleted
-  - Include branch/tag name in archive file and directory name
-  - Add dropzone upload progress
-  - Add a label for merged branches on branches page (Florent Baldino)
-  - Detect .mkd and .mkdn files as markdown (Ben Boeckel)
-  - Fix: User search feature in admin area does not respect filters
-  - Set max-width for README, issue and merge request description for easier read on big screens
-  - Update Flowdock integration to support new Flowdock API (Boyan Tabakov)
-  - Remove author from files view (Sven Strickroth)
-  - Fix infinite loop when SAML was incorrectly configured.
-
-v 7.13.5
-  - Satellites reverted
-
-v 7.13.4
-  - Allow users to send abuse reports
-
-v 7.13.3
-  - Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
-  - Allow users to send abuse reports
-  - Remove satellites
-  - Link username to profile on Group Members page (Tom Webster)
-
-v 7.13.2
-  - Fix randomly failed spec
-  - Create project services on Project creation
-  - Add admin_merge_request ability to Developer level and up
-  - Fix Error 500 when browsing projects with no HEAD (Stan Hu)
-  - Fix labels / assignee / milestone for the merge requests when issues are disabled
-  - Show the first tab automatically on MergeRequests#new
-  - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
-  - Fix Gmail Actions
-
-v 7.13.1
-  - Fix: Label modifications are not reflected in existing notes and in the issue list
-  - Fix: Label not shown in the Issue list, although it's set through web interface
-  - Fix: Group/project references are linked incorrectly
-  - Improve documentation
-  - Fix of migration: Check if session_expire_delay column exists before adding the column
-  - Fix: ActionView::Template::Error
-  - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch
-  - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project.
-  - Render Note field hints consistently for "new" and "edit" forms
-
-v 7.13.0
-  - Remove repository graph log to fix slow cache updates after push event (Stan Hu)
-  - Only enable HSTS header for HTTPS and port 443 (Stan Hu)
-  - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu)
-  - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt)
-  - Add branch switching support for graphs (Daniel Gerhardt)
-  - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
-  - Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
-  - Add support for unlocking users in admin settings (Stan Hu)
-  - Add Irker service configuration options (Stan Hu)
-  - Fix order of issues imported from GitHub (Hiroyuki Sato)
-  - Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
-  - Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
-  - Add `two_factor_enabled` field to admin user API (Stan Hu)
-  - Fix invalid timestamps in RSS feeds (Rowan Wookey)
-  - Fix downloading of patches on public merge requests when user logged out (Stan Hu)
-  - Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu)
-  - Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu)
-  - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
-  - Support commenting on diffs in side-by-side mode (Stan Hu)
-  - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
-  - Return 40x error codes if branch could not be deleted in UI (Stan Hu)
-  - Remove project visibility icons from dashboard projects list
-  - Rename "Design" profile settings page to "Preferences".
-  - Allow users to customize their default Dashboard page.
-  - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
-  - Admin can edit and remove user identities
-  - Convert CRLF newlines to LF when committing using the web editor.
-  - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged.
-  - Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled.
-  - Show a user's Two-factor Authentication status in the administration area.
-  - Explicit error when commit not found in the CI
-  - Improve performance for issue and merge request pages
-  - Users with guest access level can not set assignee, labels or milestones for issue and merge request
-  - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
-  - Better performance for pages with events list, issues list and commits list
-  - Faster automerge check and merge itself when source and target branches are in same repository
-  - Correctly show anonymous authorized applications under Profile > Applications.
-  - Query Optimization in MySQL.
-  - Allow users to be blocked and unblocked via the API
-  - Use native Postgres database cleaning during backup restore
-  - Redesign project page. Show README as default instead of activity. Move project activity to separate page
-  - Make left menu more hierarchical and less contextual by adding back item at top
-  - A fork can’t have a visibility level that is greater than the original project.
-  - Faster code search in repository and wiki. Fixes search page timeout for big repositories
-  - Allow administrators to disable 2FA for a specific user
-  - Add error message for SSH key linebreaks
-  - Store commits count in database (will populate with valid values only after first push)
-  - Rebuild cache after push to repository in background job
-  - Fix transferring of project to another group using the API.
-
-v 7.12.2
-  - Correctly show anonymous authorized applications under Profile > Applications.
-  - Faster automerge check and merge itself when source and target branches are in same repository
-  - Audit log for user authentication
-  - Allow custom label to be set for authentication providers.
-
-v 7.12.1
-  - Fix error when deleting a user who has projects (Stan Hu)
-  - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
-  - Add SAML to list of social_provider (Matt Firtion)
-  - Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets)
-  - Fix closed merge request scope at milestone page (Dmitriy Zaporozhets)
-  - Revert merge request states renaming
-  - Fix hooks for web based events with external issue references (Daniel Gerhardt)
-  - Improve performance for issue and merge request pages
-  - Compress database dumps to reduce backup size
-
-v 7.12.0
-  - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
-  - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
-  - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
-  - Update oauth button logos for Twitter and Google to recommended assets
-  - Update browser gem to version 0.8.0 for IE11 support (Stan Hu)
-  - Fix timeout when rendering file with thousands of lines.
-  - Add "Remember me" checkbox to LDAP signin form.
-  - Add session expiration delay configuration through UI application settings
-  - Don't notify users mentioned in code blocks or blockquotes.
-  - Omit link to generate labels if user does not have access to create them (Stan Hu)
-  - Show warning when a comment will add 10 or more people to the discussion.
-  - Disable changing of the source branch in merge request update API (Stan Hu)
-  - Shorten merge request WIP text.
-  - Add option to disallow users from registering any application to use GitLab as an OAuth provider
-  - Support editing target branch of merge request (Stan Hu)
-  - Refactor permission checks with issues and merge requests project settings (Stan Hu)
-  - Fix Markdown preview not working in Edit Milestone page (Stan Hu)
-  - Fix Zen Mode not closing with ESC key (Stan Hu)
-  - Allow HipChat API version to be blank and default to v2 (Stan Hu)
-  - Add file attachment support in Milestone description (Stan Hu)
-  - Fix milestone "Browse Issues" button.
-  - Set milestone on new issue when creating issue from index with milestone filter active.
-  - Make namespace API available to all users (Stan Hu)
-  - Add webhook support for note events (Stan Hu)
-  - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
-  - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
-  - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
-  - Fix git blame syntax highlighting when different commits break up lines (Stan Hu)
-  - Add "Resend confirmation e-mail" link in profile settings (Stan Hu)
-  - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka)
-  - Disabled expansion of top/bottom blobs for new file diffs
-  - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka)
-  - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka)
-  - Use the user list from the target project in a merge request (Stan Hu)
-  - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen)
-  - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen)
-  - Fix new/empty milestones showing 100% completion value (Jonah Bishop)
-  - Add a note when an Issue or Merge Request's title changes
-  - Consistently refer to MRs as either Merged or Closed.
-  - Add Merged tab to MR lists.
-  - Prefix EmailsOnPush email subject with `[Git]`.
-  - Group project contributions by both name and email.
-  - Clarify navigation labels for Project Settings and Group Settings.
-  - Move user avatar and logout button to sidebar
-  - You can not remove user if he/she is an only owner of group
-  - User should be able to leave group. If not - show him proper message
-  - User has ability to leave project
-  - Add SAML support as an omniauth provider
-  - Allow to configure a URL to show after sign out
-  - Add an option to automatically sign-in with an Omniauth provider
-  - GitLab CI service sends .gitlab-ci.yml in each push call
-  - When remove project - move repository and schedule it removal
-  - Improve group removing logic
-  - Trigger create-hooks on backup restore task
-  - Add option to automatically link omniauth and LDAP identities
-  - Allow special character in users bio. I.e.: I <3 GitLab
-
-v 7.11.4
-  - Fix missing bullets when creating lists
-  - Set rel="nofollow" on external links
-
-v 7.11.3
-  - no changes
-  - Fix upgrader script (Martins Polakovs)
-
-v 7.11.2
-  - no changes
-
-v 7.11.1
-  - no changes
-
-v 7.11.0
-  - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger)
-  - Get editing comments to work in Chrome 43 again.
-  - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu)
-  - Don't show duplicate deploy keys
-  - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger)
-  - Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
-  - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu)
-  - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
-  - Add application setting to restrict user signups to e-mail domains (Stan Hu)
-  - Don't allow a merge request to be merged when its title starts with "WIP".
-  - Add a page title to every page.
-  - Allow primary email to be set to an email that you've already added.
-  - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
-  - Ignore invalid lines in .gitmodules
-  - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
-  - Redirect to sign in page after signing out.
-  - Fix "Hello @username." references not working by no longer allowing usernames to end in period.
-  - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu)
-  - Improve project page UI
-  - Fix broken file browsing with relative submodule in personal projects (Stan Hu)
-  - Add "Reply quoting selected text" shortcut key (`r`)
-  - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
-  - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
-  - When use change branches link at MR form - save source branch selection instead of target one
-  - Improve handling of large diffs
-  - Added GitLab Event header for project hooks
-  - Add Two-factor authentication (2FA) for GitLab logins
-  - Show Atom feed buttons everywhere where applicable.
-  - Add project activity atom feed.
-  - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
-  - Explain how to get a new password reset token in welcome emails
-  - Include commit comments in MR from a forked project.
-  - Group milestones by title in the dashboard and all other issue views.
-  - Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
-  - Add default project and snippet visibility settings to the admin web UI.
-  - Show incompatible projects in Google Code import status (Stan Hu)
-  - Fix bug where commit data would not appear in some subdirectories (Stan Hu)
-  - Task lists are now usable in comments, and will show up in Markdown previews.
-  - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu)
-  - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
-  - Protect OmniAuth request phase against CSRF.
-  - Don't send notifications to mentioned users that don't have access to the project in question.
-  - Add search issues/MR by number
-  - Change plots to bar graphs in commit statistics screen
-  - Move snippets UI to fluid layout
-  - Improve UI for sidebar. Increase separation between navigation and content
-  - Improve new project command options (Ben Bodenmiller)
-  - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
-  - Prevent sending empty messages to HipChat (Chulki Lee)
-  - Improve UI for mobile phones on dashboard and project pages
-  - Add room notification and message color option for HipChat
-  - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
-  - Add footnotes support to Markdown (Guillaume Delbergue)
-  - Add current_sign_in_at to UserFull REST api.
-  - Make Sidekiq MemoryKiller shutdown signal configurable
-  - Add "Create Merge Request" buttons to commits and branches pages and push event.
-  - Show user roles by comments.
-  - Fix automatic blocking of auto-created users from Active Directory.
-  - Call merge request webhook for each new commits (Arthur Gautier)
-  - Use SIGKILL by default in Sidekiq::MemoryKiller
-  - Fix mentioning of private groups.
-  - Add style for <kbd> element in markdown
-  - Spin spinner icon next to "Checking for CI status..." on MR page.
-  - Fix reference links in dashboard activity and ATOM feeds.
-  - Ensure that the first added admin performs repository imports
-
-v 7.10.4
-  - Fix migrations broken in 7.10.2
-  - Make tags for GitLab installations running on MySQL case sensitive
-  - Get Gitorious importer to work again.
-  - Fix adding new group members from admin area
-  - Fix DB error when trying to tag a repository (Stan Hu)
-  - Fix Error 500 when searching Wiki pages (Stan Hu)
-  - Unescape branch names in compare commit (Stan Hu)
-  - Order commit comments chronologically in API.
-
-v 7.10.2
-  - Fix CI links on MR page
-
-v 7.10.0
-  - Ignore submodules that are defined in .gitmodules but are checked in as directories.
-  - Allow projects to be imported from Google Code.
-  - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger)
-  - Allow users to be invited by email to join a group or project.
-  - Don't crash when project repository doesn't exist.
-  - Add config var to block auto-created LDAP users.
-  - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
-  - Set EmailsOnPush reply-to address to committer email when enabled.
-  - Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
-  - Fix persistent XSS vulnerability around profile website URLs.
-  - Fix project import URL regex to prevent arbitary local repos from being imported.
-  - Fix directory traversal vulnerability around uploads routes.
-  - Fix directory traversal vulnerability around help pages.
-  - Don't leak existence of project via search autocomplete.
-  - Don't leak existence of group or project via search.
-  - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu)
-  - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu)
-  - Add a rake task to check repository integrity with `git fsck`
-  - Add ability to configure Reply-To address in gitlab.yml (Stan Hu)
-  - Move current user to the top of the list in assignee/author filters (Stan Hu)
-  - Fix broken side-by-side diff view on merge request page (Stan Hu)
-  - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
-  - Allow HTML tags in Markdown input
-  - Fix code unfold not working on Compare commits page (Stan Hu)
-  - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
-  - Fix "Import projects from" button to show the correct instructions (Stan Hu)
-  - Fix dots in Wiki slugs causing errors (Stan Hu)
-  - Make maximum attachment size configurable via Application Settings (Stan Hu)
-  - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
-  - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
-  - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
-  - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
-  - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
-  - Fix a link in the patch update guide
-  - Add a service to support external wikis (Hannes Rosenögger)
-  - Omit the "email patches" link and fix plain diff view for merge commits
-  - List new commits for newly pushed branch in activity view.
-  - Add sidetiq gem dependency to match EE
-  - Add changelog, license and contribution guide links to project tab bar.
-  - Improve diff UI
-  - Fix alignment of navbar toggle button (Cody Mize)
-  - Fix checkbox rendering for nested task lists
-  - Identical look of selectboxes in UI
-  - Upgrade the gitlab_git gem to version 7.1.3
-  - Move "Import existing repository by URL" option to button.
-  - Improve error message when save profile has error.
-  - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+)
-  - Add location field to user profile
-  - Fix print view for markdown files and wiki pages
-  - Fix errors when deleting old backups
-  - Improve GitLab performance when working with git repositories
-  - Add tag message and last commit to tag hook (Kamil Trzciński)
-  - Restrict permissions on backup files
-  - Improve oauth accounts UI in profile page
-  - Add ability to unlink connected accounts
-  - Replace commits calendar with faster contribution calendar that includes issues and merge requests
-  - Add inifinite scroll to user page activity
-  - Don't include system notes in issue/MR comment count.
-  - Don't mark merge request as updated when merge status relative to target branch changes.
-  - Link note avatar to user.
-  - Make Git-over-SSH errors more descriptive.
-  - Fix EmailsOnPush.
-  - Refactor issue filtering
-  - AJAX selectbox for issue assignee and author filters
-  - Fix issue with missing options in issue filtering dropdown if selected one
-  - Prevent holding Control-Enter or Command-Enter from posting comment multiple times.
-  - Prevent note form from being cleared when submitting failed.
-  - Improve file icons rendering on tree (Sullivan Sénéchal)
-  - API: Add pagination to project events
-  - Get issue links in notification mail to work again.
-  - Don't show commit comment button when user is not signed in.
-  - Fix admin user projects lists.
-  - Don't leak private group existence by redirecting from namespace controller to group controller.
-  - Ability to skip some items from backup (database, respositories or uploads)
-  - Archive repositories in background worker.
-  - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace.
-  - Project labels are now available over the API under the "tag_list" field (Cristian Medina)
-  - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz)
-  - Fix and improve help rendering (Sullivan Sénéchal)
-  - Fix final line in EmailsOnPush email diff being rendered as error.
-  - Prevent duplicate Buildkite service creation.
-  - Fix git over ssh errors 'fatal: protocol error: bad line length character'
-  - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled
-  - Bust group page project list cache when namespace name or path changes.
-  - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded
-  - Allow user to choose a public email to show on public profile
-  - Remove truncation from issue titles on milestone page (Jason Blanchard)
-  - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
-  - Fix merge request comments on files with multiple commits
-  - Fix Resource Owner Password Authentication Flow
-  - Add icons to Add dropdown items.
-  - Allow admin to create public deploy keys that are accessible to any project.
-  - Warn when gitlab-shell version doesn't match requirement.
-  - Skip email confirmation when set by admin or via LDAP.
-  - Only allow users to reference groups, projects, issues, MRs, commits they have access to.
-
-v 7.9.4
-  - Security: Fix project import URL regex to prevent arbitary local repos from being imported
-  - Fixed issue where only 25 commits would load in file listings
-  - Fix LDAP identities  after config update
-
-v 7.9.3
-  - Contains no changes
-
-v 7.9.2
-  - Contains no changes
-
-v 7.9.1
-  - Include missing events and fix save functionality in admin service template settings form (Stan Hu)
-  - Fix "Import projects from" button to show the correct instructions (Stan Hu)
-  - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
-  - Fix for LDAP with commas in DN
-  - Fix missing events and in admin Slack service template settings form (Stan Hu)
-  - Don't show commit comment button when user is not signed in.
-  - Downgrade gemnasium-gitlab-service gem
-
-v 7.9.0
-  - Add HipChat integration documentation (Stan Hu)
-  - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
-  - Fix broken email images (Hannes Rosenögger)
-  - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg)
-  - Fix mass SQL statements on initial push (Hannes Rosenögger)
-  - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu)
-  - Add comment notification events to HipChat and Slack services (Stan Hu)
-  - Add issue and merge request events to HipChat and Slack services (Stan Hu)
-  - Fix merge request URL passed to Webhooks. (Stan Hu)
-  - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
-  - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
-  - Move labels/milestones tabs to sidebar
-  - Upgrade Rails gem to version 4.1.9.
-  - Improve error messages for file edit failures
-  - Improve UI for commits, issues and merge request lists
-  - Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
-  - Allow admins to override restricted project visibility settings.
-  - Move restricted visibility settings from gitlab.yml into the web UI.
-  - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
-  - Save web edit in new branch
-  - Fix ordering of imported but unchanged projects (Marco Wessel)
-  - Mobile UI improvements: make aside content expandable
-  - Expose avatar_url in projects API
-  - Fix checkbox alignment on the application settings page.
-  - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
-  - Fix mass-unassignment of issues (Robert Speicher)
-  - Fix hidden diff comments in merge request discussion view
-  - Allow user confirmation to be skipped for new users via API
-  - Add a service to send updates to an Irker gateway (Romain Coltel)
-  - Add brakeman (security scanner for Ruby on Rails)
-  - Slack username and channel options
-  - Add grouped milestones from all projects to dashboard.
-  - Webhook sends pusher email as well as commiter
-  - Add Bitbucket omniauth provider.
-  - Add Bitbucket importer.
-  - Support referencing issues to a project whose name starts with a digit
-  - Condense commits already in target branch when updating merge request source branch.
-  - Send notifications and leave system comments when bulk updating issues.
-  - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
-  - Move groups page from profile to dashboard
-  - Starred projects page at dashboard
-  - Blocking user does not remove him/her from project/groups but show blocked label
-  - Change subject of EmailsOnPush emails to include namespace, project and branch.
-  - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed.
-  - Remove confusing footer from EmailsOnPush mail body.
-  - Add list of changed files to EmailsOnPush emails.
-  - Add option to send EmailsOnPush emails from committer email if domain matches.
-  - Add option to disable code diffs in EmailOnPush emails.
-  - Wrap commit message in EmailsOnPush email.
-  - Send EmailsOnPush emails when deleting commits using force push.
-  - Fix EmailsOnPush email comparison link to include first commit.
-  - Fix highliht of selected lines in file
-  - Reject access to group/project avatar if the user doesn't have access.
-  - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update)
-  - Add GitLab active users count to rake gitlab:check
-  - Starred projects page at dashboard
-  - Make email display name configurable
-  - Improve json validation in hook data
-  - Use Emoji One
-  - Updated emoji help documentation to properly reference EmojiOne.
-  - Fix missing GitHub organisation repositories on import page.
-  - Added blue theme
-  - Remove annoying notice messages when create/update merge request
-  - Allow smb:// links in Markdown text.
-  - Filter merge request by title or description at Merge Requests page
-  - Block user if he/she was blocked in Active Directory
-  - Fix import pages not working after first load.
-  - Use custom LDAP label in LDAP signin form.
-  - Execute hooks and services when branch or tag is created or deleted through web interface.
-  - Block and unblock user if he/she was blocked/unblocked in Active Directory
-  - Raise recommended number of unicorn workers from 2 to 3
-  - Use same layout and interactivity for project members as group members.
-  - Prevent gitlab-shell character encoding issues by receiving its changes as raw data.
-  - Ability to unsubscribe/subscribe to issue or merge request
-  - Delete deploy key when last connection to a project is destroyed.
-  - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther)
-  - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup)
-  - Add canceled status for CI
-  - Send EmailsOnPush email when branch or tag is created or deleted.
-  - Faster merge request processing for large repository
-  - Prevent doubling AJAX request with each commit visit via Turbolink
-  - Prevent unnecessary doubling of js events on import pages and user calendar
-
-v 7.8.4
-  - Fix issue_tracker_id substitution in custom issue trackers
-  - Fix path and name duplication in namespaces
-
-v 7.8.3
-  - Bump version of gitlab_git fixing annotated tags without message
-
-v 7.8.2
-  - Fix service migration issue when upgrading from versions prior to 7.3
-  - Fix setting of the default use project limit via admin UI
-  - Fix showing of already imported projects for GitLab and Gitorious importers
-  - Fix response of push to repository to return "Not found" if user doesn't have access
-  - Fix check if user is allowed to view the file attachment
-  - Fix import check for case sensetive namespaces
-  - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time.
-  - Properly handle autosave local storage exceptions.
-  - Escape wildcards when searching LDAP by username.
-
-v 7.8.1
-  - Fix run of custom post receive hooks
-  - Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3
-  - Fix the warning for LDAP users about need to set password
-  - Fix avatars which were not shown for non logged in users
-  - Fix urls for the issues when relative url was enabled
-
-v 7.8.0
-  - Fix access control and protection against XSS for note attachments and other uploads.
-  - Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
-  - Make project search case insensitive (Hannes Rosenögger)
-  - Include issue/mr participants in list of recipients for reassign/close/reopen emails
-  - Expose description in groups API
-  - Better UI for project services page
-  - Cleaner UI for web editor
-  - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger)
-  - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
-  - View note image attachments in new tab when clicked instead of downloading them
-  - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
-  - Fix overflow at sidebar when have several items
-  - Add notes for label changes in issue and merge requests
-  - Show tags in commit view (Hannes Rosenögger)
-  - Only count a user's vote once on a merge request or issue (Michael Clarke)
-  - Increase font size when browse source files and diffs
-  - Service Templates now let you set default values for all services
-  - Create new file in empty repository using GitLab UI
-  - Ability to clone project using oauth2 token
-  - Upgrade Sidekiq gem to version 3.3.0
-  - Stop git zombie creation during force push check
-  - Show success/error messages for test setting button in services
-  - Added Rubocop for code style checks
-  - Fix commits pagination
-  - Async load a branch information at the commit page
-  - Disable blacklist validation for project names
-  - Allow configuring protection of the default branch upon first push (Marco Wessel)
-  - Add gitlab.com importer
-  - Add an ability to login with gitlab.com
-  - Add a commit calendar to the user profile (Hannes Rosenögger)
-  - Submit comment on command-enter
-  - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
-  - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
-  - Fix long broadcast message cut-off on left sidebar (Visay Keo)
-  - Add Project Avatars (Steven Thonus and Hannes Rosenögger)
-  - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
-  - Edit group members via API
-  - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks)
-  - Add action property to merge request hook (Julien Bianchi)
-  - Remove duplicates from group milestone participants list.
-  - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger)
-  - API: Access groups with their path (Julien Bianchi)
-  - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard)
-  - Allow notification email to be set separately from primary email.
-  - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
-  - Don't have Markdown preview fail for long comments/wiki pages.
-  - When test webhook - show error message instead of 500 error page if connection to hook url was reset
-  - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
-  - Added persistent collapse button for left side nav bar (Jason Blanchard)
-  - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
-  - Don't allow page to be scaled on mobile.
-  - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up.
-  - Show assignees in merge request index page (Kelvin Mutuma)
-  - Link head panel titles to relevant root page.
-  - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S).
-  - Show users button to share their newly created public or internal projects on twitter
-  - Add quick help links to the GitLab pricing and feature comparison pages.
-  - Fix duplicate authorized applications in user profile and incorrect application client count in admin area.
-  - Make sure Markdown previews always use the same styling as the eventual destination.
-  - Remove deprecated Group#owner_id from API
-  - Show projects user contributed to on user page. Show stars near project on user page.
-  - Improve database performance for GitLab
-  - Add Asana service (Jeremy Benoist)
-  - Improve project webhooks with extra data
-
-v 7.7.2
-  - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
-  - Fix issue when LDAP user can't login with existing GitLab account
-
-v 7.7.1
-  - Improve mention autocomplete performance
-  - Show setup instructions for GitHub import if disabled
-  - Allow use http for OAuth applications
-
-v 7.7.0
-  - Import from GitHub.com feature
-  - Add Jetbrains Teamcity CI service (Jason Lippert)
-  - Mention notification level
-  - Markdown preview in wiki (Yuriy Glukhov)
-  - Raise group avatar filesize limit to 200kb
-  - OAuth applications feature
-  - Show user SSH keys in admin area
-  - Developer can push to protected branches option
-  - Set project path instead of project name in create form
-  - Block Git HTTP access after 10 failed authentication attempts
-  - Updates to the messages returned by API (sponsored by O'Reilly Media)
-  - New UI layout with side navigation
-  - Add alert message in case of outdated browser (IE < 10)
-  - Added API support for sorting projects
-  - Update gitlab_git to version 7.0.0.rc14
-  - Add API project search filter option for authorized projects
-  - Fix File blame not respecting branch selection
-  - Change some of application settings on fly in admin area UI
-  - Redesign signin/signup pages
-  - Close standard input in Gitlab::Popen.popen
-  - Trigger GitLab CI when push tags
-  - When accept merge request - do merge using sidaekiq job
-  - Enable web signups by default
-  - Fixes for diff comments: drag-n-drop images, selecting images
-  - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
-  - Remove password strength indicator
-
-v 7.6.0
-  - Fork repository to groups
-  - New rugged version
-  - Add CRON=1 backup setting for quiet backups
-  - Fix failing wiki restore
-  - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable)
-  - Monokai highlighting style now more faithful to original design (Mark Riedesel)
-  - Create project with repository in synchrony
-  - Added ability to create empty repo or import existing one if project does not have repository
-  - Reactivate highlight.js language autodetection
-  - Mobile UI improvements
-  - Change maximum avatar file size from 100KB to 200KB
-  - Strict validation for snippet file names
-  - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada)
-  - In the docker directory is a container template based on the Omnibus packages.
-  - Update Sidekiq to version 2.17.8
-  - Add author filter to project issues and merge requests pages
-  - Atom feed for user activity
-  - Support multiple omniauth providers for the same user
-  - Rendering cross reference in issue title and tooltip for merge request
-  - Show username in comments
-  - Possibility to create Milestones or Labels when Issues are disabled
-  - Fix bug with showing gpg signature in tag
-
-v 7.5.3
-  - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
-
-v 7.5.2
-  - Don't log Sidekiq arguments by default
-  - Fix restore of wiki repositories from backups
-
-v 7.5.1
-  - Add missing timestamps to 'members' table
-
-v 7.5.0
-  - API: Add support for Hipchat (Kevin Houdebert)
-  - Add time zone configuration in gitlab.yml (Sullivan Senechal)
-  - Fix LDAP authentication for Git HTTP access
-  - Run 'GC.start' after every EmailsOnPushWorker job
-  - Fix LDAP config lookup for provider 'ldap'
-  - Drop all sequences during Postgres database restore
-  - Project title links to project homepage (Ben Bodenmiller)
-  - Add Atlassian Bamboo CI service (Drew Blessing)
-  - Mentioned @user will receive email even if he is not participating in issue or commit
-  - Session API: Use case-insensitive authentication like in UI (Andrey Krivko)
-  - Tie up loose ends with annotated tags: API & UI (Sean Edge)
-  - Return valid json for deleting branch via API (sponsored by O'Reilly Media)
-  - Expose username in project events API (sponsored by O'Reilly Media)
-  - Adds comments to commits in the API
-  - Performance improvements
-  - Fix post-receive issue for projects with deleted forks
-  - New gitlab-shell version with custom hooks support
-  - Improve code
-  - GitLab CI 5.2+ support (does not support older versions)
-  - Fixed bug when you can not push commits starting with 000000 to protected branches
-  - Added a password strength indicator
-  - Change project name and path in one form
-  - Display renamed files in diff views (Vinnie Okada)
-  - Fix raw view for public snippets
-  - Use secret token with GitLab internal API.
-  - Add missing timestamps to 'members' table
-
-v 7.4.5
-  - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
-
-v 7.4.4
-  - No changes
-
-v 7.4.3
-  - Fix raw snippets view
-  - Fix security issue for member api
-  - Fix buildbox integration
-
-v 7.4.2
-  - Fix internal snippet exposing for unauthenticated users
-
-v 7.4.1
-  - Fix LDAP authentication for Git HTTP access
-  - Fix LDAP config lookup for provider 'ldap'
-  - Fix public snippets
-  - Fix 500 error on projects with nested submodules
-
-v 7.4.0
-  - Refactored membership logic
-  - Improve error reporting on users API (Julien Bianchi)
-  - Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
-  - Default branch is protected by default
-  - Increase unicorn timeout to 60 seconds
-  - Sort search autocomplete projects by stars count so most popular go first
-  - Add README to tab on project show page
-  - Do not delete tmp/repositories itself during clean-up, only its contents
-  - Support for backup uploads to remote storage
-  - Prevent notes polling when there are not notes
-  - Internal ForkService: Prepare support for fork to a given namespace
-  - API: Add support for forking a project via the API (Bernhard Kaindl)
-  - API: filter project issues by milestone (Julien Bianchi)
-  - Fail harder in the backup script
-  - Changes to Slack service structure, only webhook url needed
-  - Zen mode for wiki and milestones (Robert Schilling)
-  - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling)
-  - Font Awesome 4.2 integration (Sullivan Senechal)
-  - Add Pushover service integration (Sullivan Senechal)
-  - Add select field type for services options (Sullivan Senechal)
-  - Add cross-project references to the Markdown parser (Vinnie Okada)
-  - Add task lists to issue and merge request descriptions (Vinnie Okada)
-  - Snippets can be public, internal or private
-  - Improve danger zone: ask project path to confirm data-loss action
-  - Raise exception on forgery
-  - Show build coverage in Merge Requests (requires GitLab CI v5.1)
-  - New milestone and label links on issue edit form
-  - Improved repository graphs
-  - Improve event note display in dashboard and project activity views (Vinnie Okada)
-  - Add users sorting to admin area
-  - UI improvements
-  - Fix ambiguous sha problem with mentioned commit
-  - Fixed bug with apostrophe when at mentioning users
-  - Add active directory ldap option
-  - Developers can push to wiki repo. Protected branches does not affect wiki repo any more
-  - Faster rev list
-  - Fix branch removal
-
-v 7.3.2
-  - Fix creating new file via web editor
-  - Use gitlab-shell v2.0.1
-
-v 7.3.1
-  - Fix ref parsing in Gitlab::GitAccess
-  - Fix error 500 when viewing diff on a file with changed permissions
-  - Fix adding comments to MR when source branch is master
-  - Fix error 500 when searching description contains relative link
-
-v 7.3.0
-  - Always set the 'origin' remote in satellite actions
-  - Write authorized_keys in tmp/ during tests
-  - Use sockets to connect to Redis
-  - Add dormant New Relic gem (can be enabled via environment variables)
-  - Expire Rack sessions after 1 week
-  - Cleaner signin/signup pages
-  - Improved comments UI
-  - Better search with filtering, pagination etc
-  - Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov)
-  - Prevent project stars duplication when fork project
-  - Use the default Unicorn socket backlog value of 1024
-  - Support Unix domain sockets for Redis
-  - Store session Redis keys in 'session:gitlab:' namespace
-  - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
-  - Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy)
-  - Keyboard shortcuts for productivity (Robert Schilling)
-  - API: filter issues by state (Julien Bianchi)
-  - API: filter issues by labels (Julien Bianchi)
-  - Add system hook for ssh key changes
-  - Add blob permalink link (Ciro Santilli)
-  - Create annotated tags through UI and API (Sean Edge)
-  - Snippets search (Charles Bushong)
-  - Comment new push to existing MR
-  - Add 'ci' to the blacklist of forbidden names
-  - Improve text filtering on issues page
-  - Comment & Close button
-  - Process git push --all much faster
-  - Don't allow edit of system notes
-  - Project wiki search (Ralf Seidler)
-  - Enabled Shibboleth authentication support (Matus Banas)
-  - Zen mode (fullscreen) for issues/MR/notes (Robert Schilling)
-  - Add ability to configure webhook timeout via gitlab.yml (Wes Gurney)
-  - Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media)
-  - Add Redis socket support to 'rake gitlab:shell:install'
-
-v 7.2.1
-  - Delete orphaned labels during label migration (James Brooks)
-  - Security: prevent XSS with stricter MIME types for raw repo files
-
-v 7.2.0
-  - Explore page
-  - Add project stars (Ciro Santilli)
-  - Log Sidekiq arguments
-  - Better labels: colors, ability to rename and remove
-  - Improve the way merge request collects diffs
-  - Improve compare page for large diffs
-  - Expose the full commit message via API
-  - Fix 500 error on repository rename
-  - Fix bug when MR download patch return invalid diff
-  - Test gitlab-shell integration
-  - Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported
-  - API for labels (Robert Schilling)
-  - API: ability to set an import url when creating project for specific user
-
-v 7.1.1
-  - Fix cpu usage issue in Firefox
-  - Fix redirect loop when changing password by new user
-  - Fix 500 error on new merge request page
-
-v 7.1.0
-  - Remove observers
-  - Improve MR discussions
-  - Filter by description on Issues#index page
-  - Fix bug with namespace select when create new project page
-  - Show README link after description for non-master members
-  - Add @all mention for comments
-  - Dont show reply button if user is not signed in
-  - Expose more information for issues with webhook
-  - Add a mention of the merge request into the default merge request commit message
-  - Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc
-  - Fix concurrency issue in repository download
-  - Dont allow repository name start with ?
-  - Improve email threading (Pierre de La Morinerie)
-  - Cleaner help page
-  - Group milestones
-  - Improved email notifications
-  - Contributors API (sponsored by Mobbr)
-  - Fix LDAP TLS authentication (Boris HUISGEN)
-  - Show VERSION information on project sidebar
-  - Improve branch removal logic when accept MR
-  - Fix bug where comment form is spawned inside the Reply button
-  - Remove Dir.chdir from Satellite#lock for thread-safety
-  - Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs!
-  - Show error message in case of timeout in satellite when create MR
-  - Show first 100 files for huge diff instead of hiding all
-  - Change default admin email from admin@local.host to admin@example.com
-
-v 7.0.0
-  - The CPU no longer overheats when you hold down the spacebar
-  - Improve edit file UI
-  - Add ability to upload group avatar when create
-  - Protected branch cannot be removed
-  - Developers can remove normal branches with UI
-  - Remove branch via API (sponsored by O'Reilly Media)
-  - Move protected branches page to Project settings area
-  - Redirect to Files view when create new branch via UI
-  - Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso)
-  - Refactor the markdown relative links processing
-  - Make it easier to implement other CI services for GitLab
-  - Group masters can create projects in group
-  - Deprecate ruby 1.9.3 support
-  - Only masters can rewrite/remove git tags
-  - Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible
-  - UI improvements
-  - Case-insensetive search for issues
-  - Update to rails 4.1
-  - Improve performance of application for projects and groups with a lot of members
-  - Formally support Ruby 2.1
-  - Include Nginx gitlab-ssl config
-  - Add manual language detection for highlight.js
-  - Added example.com/:username routing
-  - Show notice if your profile is public
-  - UI improvements for mobile devices
-  - Improve diff rendering performance
-  - Drag-n-drop for issues and merge requests between states at milestone page
-  - Fix '0 commits' message for huge repositories on project home page
-  - Prevent 500 error page when visit commit page from large repo
-  - Add notice about huge push over http to unicorn config
-  - File action in satellites uses default 30 seconds timeout instead of old 10 seconds one
-  - Overall performance improvements
-  - Skip init script check on omnibus-gitlab
-  - Be more selective when killing stray Sidekiqs
-  - Check LDAP user filter during sign-in
-  - Remove wall feature (no data loss - you can take it from database)
-  - Dont expose user emails via API unless you are admin
-  - Detect issues closed by Merge Request description
-  - Better email subject lines from email on push service (Alex Elman)
-  - Enable identicon for gravatar be default
-
-v 6.9.2
-  - Revert the commit that broke the LDAP user filter
-
-v 6.9.1
-  - Fix scroll to highlighted line
-  - Fix the pagination on load for commits page
-
-v 6.9.0
-  - Store Rails cache data in the Redis `cache:gitlab` namespace
-  - Adjust MySQL limits for existing installations
-  - Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed)
-  - Markdown preview or diff during editing via web editor (Evgeniy Sokovikov)
-  - Give the Rails cache its own Redis namespace
-  - Add ability to set different ssh host, if different from http/https
-  - Fix syntax highlighting for code comments blocks
-  - Improve comments loading logic
-  - Stop refreshing comments when the tab is hidden
-  - Improve issue and merge request mobile UI (Drew Blessing)
-  - Document how to convert a backup to PostgreSQL
-  - Fix locale bug in backup manager
-  - Fix can not automerge when MR description is too long
-  - Fix wiki backup skip bug
-  - Two Step MR creation process
-  - Remove unwanted files from satellite working directory with git clean -fdx
-  - Accept merge request via API (sponsored by O'Reilly Media)
-  - Add more access checks during API calls
-  - Block SSH access for 'disabled' Active Directory users
-  - Labels for merge requests (Drew Blessing)
-  - Threaded emails by setting a Message-ID (Philip Blatter)
-
-v 6.8.0
-  - Ability to at mention users that are participating in issue and merge req. discussion
-  - Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu)
-  - Make user search case-insensitive (Christopher Arnold)
-  - Remove omniauth-ldap nickname bug workaround
-  - Drop all tables before restoring a Postgres backup
-  - Make the repository downloads path configurable
-  - Create branches via API (sponsored by O'Reilly Media)
-  - Changed permission of gitlab-satellites directory not to be world accessible
-  - Protected branch does not allow force push
-  - Fix popen bug in `rake gitlab:satellites:create`
-  - Disable connection reaping for MySQL
-  - Allow oauth signup without email for twitter and github
-  - Fix faulty namespace names that caused 500 on user creation
-  - Option to disable standard login
-  - Clean old created archives from repository downloads directory
-  - Fix download link for huge MR diffs
-  - Expose event and mergerequest timestamps in API
-  - Fix emails on push service when only one commit is pushed
-
-v 6.7.3
-  - Fix the merge notification email not being sent (Pierre de La Morinerie)
-  - Drop all tables before restoring a Postgres backup
-  - Remove yanked modernizr gem
-
-v 6.7.2
-  - Fix upgrader script
-
-v 6.7.1
-  - Fix GitLab CI integration
-
-v 6.7.0
-  - Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations
-  - Add support for Gemnasium as a Project Service (Olivier Gonzalez)
-  - Add edit file button to MergeRequest diff
-  - Public groups (Jason Hollingsworth)
-  - Cleaner headers in Notification Emails (Pierre de La Morinerie)
-  - Blob and tree gfm links to anchors work
-  - Piwik Integration (Sebastian Winkler)
-  - Show contribution guide link for new issue form (Jeroen van Baarsen)
-  - Fix CI status for merge requests from fork
-  - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
-  - New page load indicator that includes a spinner that scrolls with the page
-  - Converted all the help sections into markdown
-  - LDAP user filters
-  - Streamline the content of notification emails (Pierre de La Morinerie)
-  - Fixes a bug with group member administration (Matt DeTullio)
-  - Sort tag names using VersionSorter (Robert Speicher)
-  - Add GFM autocompletion for MergeRequests (Robert Speicher)
-  - Add webhook when a new tag is pushed (Jeroen van Baarsen)
-  - Add button for toggling inline comments in diff view
-  - Add retry feature for repository import
-  - Reuse the GitLab LDAP connection within each request
-  - Changed markdown new line behaviour to conform to markdown standards
-  - Fix global search
-  - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5)
-  - Create and Update MR calls now support the description parameter (Greg Messner)
-  - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository
-  - Added Slack service integration (Federico Ravasio)
-  - Better API responses for access_levels (sponsored by O'Reilly Media)
-  - Requires at least 2 unicorn workers
-  - Requires gitlab-shell v1.9+
-  - Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License)
-  - Fix `/:username.keys` response content type (Dmitry Medvinsky)
-
-v 6.6.5
-  - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
-  - Hide mr close button for comment form if merge request was closed or inline comment
-  - Adds ability to reopen closed merge request
-
-v 6.6.4
-  - Add missing html escape for highlighted code blocks in comments, issues
-
-v 6.6.3
-  - Fix 500 error when edit yourself from admin area
-  - Hide private groups for public profiles
-
-v 6.6.2
-  - Fix 500 error on branch/tag create or remove via UI
-
-v 6.6.1
-  - Fix 500 error on files tab if submodules presents
-
-v 6.6.0
-  - Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys
-  - Permissions: Developer now can manage issue tracker (modify any issue)
-  - Improve Code Compare page performance
-  - Group avatar
-  - Pygments.rb replaced with highlight.js
-  - Improve Merge request diff store logic
-  - Improve render performnace for MR show page
-  - Fixed Assembla hardcoded project name
-  - Jira integration documentation
-  - Refactored app/services
-  - Remove snippet expiration
-  - Mobile UI improvements (Drew Blessing)
-  - Fix block/remove UI for admin::users#show page
-  - Show users' group membership on users' activity page (Robert Djurasaj)
-  - User pages are visible without login if user is authorized to a public project
-  - Markdown rendered headers have id derived from their name and link to their id
-  - Improve application to work faster with large groups (100+ members)
-  - Multiple emails per user
-  - Show last commit for file when view file source
-  - Restyle Issue#show page and MR#show page
-  - Ability to filter by multiple labels for Issues page
-  - Rails version to 4.0.3
-  - Fixed attachment identifier displaying underneath note text (Jason Blanchard)
-
-v 6.5.1
-  - Fix branch selectbox when create merge request from fork
-
-v 6.5.0
-  - Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard)
-  - Add color custimization and previewing to broadcast messages
-  - Fixed notes anchors
-  - Load new comments in issues dynamically
-  - Added sort options to Public page
-  - New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media)
-  - Add project visibility icons to dashboard
-  - Enable secure cookies if https used
-  - Protect users/confirmation with rack_attack
-  - Default HTTP headers to protect against MIME-sniffing, force https if enabled
-  - Bootstrap 3 with responsive UI
-  - New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth)
-  - Restyled accept widgets for MR
-  - SCSS refactored
-  - Use jquery timeago plugin
-  - Fix 500 error for rdoc files
-  - Ability to customize merge commit message (sponsored by Say Media)
-  - Search autocomplete via ajax
-  - Add website url to user profile
-  - Files API supports base64 encoded content (sponsored by O'Reilly Media)
-  - Added support for Go's repository retrieval (Bruno Albuquerque)
-
-v 6.4.3
-  - Don't use unicorn worker killer if PhusionPassenger is defined
-
-v 6.4.2
-  - Fixed wrong behaviour of script/upgrade.rb
-
-v 6.4.1
-  - Fixed bug with repository rename
-  - Fixed bug with project transfer
-
-v 6.4.0
-  - Added sorting to project issues page (Jason Blanchard)
-  - Assembla integration (Carlos Paramio)
-  - Fixed another 500 error with submodules
-  - UI: More compact issues page
-  - Minimal password length increased to 8 symbols
-  - Side-by-side diff view (Steven Thonus)
-  - Internal projects (Jason Hollingsworth)
-  - Allow removal of avatar (Drew Blessing)
-  - Project webhooks now support issues and merge request events
-  - Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
-  - Expire event cache on avatar creation/removal (Drew Blessing)
-  - Archiving old projects (Steven Thonus)
-  - Rails 4
-  - Add time ago tooltips to show actual date/time
-  - UI: Fixed UI for admin system hooks
-  - Ruby script for easier GitLab upgrade
-  - Do not remove Merge requests if fork project was removed
-  - Improve sign-in/signup UX
-  - Add resend confirmation link to sign-in page
-  - Set noreply@HOSTNAME for reply_to field in all emails
-  - Show GitLab API version on Admin#dashboard
-  - API Cross-origin resource sharing
-  - Show READMe link at project home page
-  - Show repo size for projects in Admin area
-
-v 6.3.0
-  - API for adding gitlab-ci service
-  - Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey)
-  - Restyle project home page
-  - Grammar fixes
-  - Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev)
-  - Security improvements
-  - Added support for GitLab CI 4.0
-  - Fixed issue with 500 error when group did not exist
-  - Ability to leave project
-  - You can create file in repo using UI
-  - You can remove file from repo using UI
-  - API: dropped default_branch attribute from project during creation
-  - Project default_branch is not stored in db any more. It takes from repo now.
-  - Admin broadcast messages
-  - UI improvements
-  - Dont show last push widget if user removed this branch
-  - Fix 500 error for repos with newline in file name
-  - Extended html titles
-  - API: create/update/delete repo files
-  - Admin can transfer project to any namespace
-  - API: projects/all for admin users
-  - Fix recent branches order
-
-v 6.2.4
-  - Security: Cast API private_token to string (CVE-2013-4580)
-  - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
-  - Fix for Git SSH access for LDAP users
-
-v 6.2.3
-  - Security: More protection against CVE-2013-4489
-  - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
-  - Fix sidekiq rake tasks
-
-v 6.2.2
-  - Security: Update gitlab_git (CVE-2013-4489)
-
-v 6.2.1
-  - Security: Fix issue with generated passwords for new users
-
-v 6.2.0
-  - Public project pages are now visible to everyone (files, issues, wik, etc.)
-    THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE
-  - Add group access to permissions page
-  - Require current password to change one
-  - Group owner or admin can remove other group owners
-  - Remove group transfer since we have multiple owners
-  - Respect authorization in Repository API
-  - Improve UI for Project#files page
-  - Add more security specs
-  - Added search for projects by name to api (Izaak Alpert)
-  - Make default user theme configurable (Izaak Alpert)
-  - Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
-  - Rake tasks for webhooks management (Jonhnny Weslley)
-  - Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
-  - API: Remove group
-  - API: Remove project
-  - Avatar upload on profile page with a maximum of 100KB (Steven Thonus)
-  - Store the sessions in Redis instead of the cookie store
-  - Fixed relative links in markdown
-  - User must confirm their email if signup enabled
-  - User must confirm changed email
-
-v 6.1.0
-  - Project specific IDs for issues, mr, milestones
-    Above items will get a new id and for example all bookmarked issue urls will change.
-    Old issue urls are redirected to the new one if the issue id is too high for an internal id.
-  - Description field added to Merge Request
-  - API: Sudo api calls (Izaak Alpert)
-  - API: Group membership api (Izaak Alpert)
-  - Improved commit diff
-  - Improved large commit handling (Boyan Tabakov)
-  - Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey)
-  - Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson)
-  - Close issues automatically when pushing commits with a special message
-  - Improve user removal from admin area
-  - Invalidate events cache when project was moved
-  - Remove deprecated classes and rake tasks
-  - Add event filter for group and project show pages
-  - Add links to create branch/tag from project home page
-  - Add public-project? checkbox to new-project view
-  - Improved compare page. Added link to proceed into Merge Request
-  - Send an email to a user when they are added to group
-  - New landing page when you have 0 projects
-
-v 6.0.0
-  - Feature: Replace teams with group membership
-    We introduce group membership in 6.0 as a replacement for teams.
-    The old combination of groups and teams was confusing for a lot of people.
-    And when the members of a team where changed this wasn't reflected in the project permissions.
-    In GitLab 6.0 you will be able to add members to a group with a permission level for each member.
-    These group members will have access to the projects in that group.
-    Any changes to group members will immediately be reflected in the project permissions.
-    You can even have multiple owners for a group, greatly simplifying administration.
-  - Feature: Ability to have multiple owners for group
-  - Feature: Merge Requests between fork and project (Izaak Alpert)
-  - Feature: Generate fingerprint for ssh keys
-  - Feature: Ability to create and remove branches with UI
-  - Feature: Ability to create and remove git tags with UI
-  - Feature: Groups page in profile. You can leave group there
-  - API: Allow login with LDAP credentials
-  - Redesign: project settings navigation
-  - Redesign: snippets area
-  - Redesign: ssh keys page
-  - Redesign: buttons, blocks and other ui elements
-  - Add comment title to rss feed
-  - You can use arrows to navigate at tree view
-  - Add project filter on dashboard
-  - Cache project graph
-  - Drop support of root namespaces
-  - Default theme is classic now
-  - Cache result of methods like authorize_projects, project.team.members etc
-  - Remove $.ready events
-  - Fix onclick events being double binded
-  - Add notification level to group membership
-  - Move all project controllers/views under Projects:: module
-  - Move all profile controllers/views under Profiles:: module
-  - Apply user project limit only for personal projects
-  - Unicorn is default web server again
-  - Store satellites lock files inside satellites dir
-  - Disabled threadsafety mode in rails
-  - Fixed bug with loosing MR comments
-  - Improved MR comments logic
-  - Render readme file for projects in public area
-
-v 5.4.2
-  - Security: Cast API private_token to string (CVE-2013-4580)
-  - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
-
-v 5.4.1
-  - Security: Fixes for CVE-2013-4489
-  - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
-
-v 5.4.0
-  - Ability to edit own comments
-  - Documentation improvements
-  - Improve dashboard projects page
-  - Fixed nav for empty repos
-  - GitLab Markdown help page
-  - Misspelling fixes
-  - Added support of unicorn and fog gems
-  - Added client list to API doc
-  - Fix PostgreSQL database restoration problem
-  - Increase snippet content column size
-  - allow project import via git:// url
-  - Show participants on issues, including mentions
-  - Notify mentioned users with email
-
-v 5.3.0
-  - Refactored services
-  - Campfire service added
-  - HipChat service added
-  - Fixed bug with LDAP + git over http
-  - Fixed bug with google analytics code being ignored
-  - Improve sign-in page if ldap enabled
-  - Respect newlines in wall messages
-  - Generate the Rails secret token on first run
-  - Rename repo feature
-  - Init.d: remove gitlab.socket on service start
-  - Api: added teams api
-  - Api: Prevent blob content being escaped
-  - Api: Smart deploy key add behaviour
-  - Api: projects/owned.json return user owned project
-  - Fix bug with team assignation on project from #4109
-  - Advanced snippets: public/private, project/personal (Andrew Kulakov)
-  - Repository Graphs (Karlo Nicholas T. Soriano)
-  - Fix dashboard lost if comment on commit
-  - Update gitlab-grack. Fixes issue with --depth option
-  - Fix project events duplicate on project page
-  - Fix postgres error when displaying network graph.
-  - Fix dashboard event filter when navigate via turbolinks
-  - init.d: Ensure socket is removed before starting service
-  - Admin area: Style teams:index, group:show pages
-  - Own page for failed forking
-  - Scrum view for milestone
-
-v 5.2.0
-  - Turbolinks
-  - Git over http with ldap credentials
-  - Diff with better colors and some spacing on the corners
-  - Default values for project features
-  - Fixed huge_commit view
-  - Restyle project clone panel
-  - Move Gitlab::Git code to gitlab_git gem
-  - Move update docs in repo
-  - Requires gitlab-shell v1.4.0
-  - Fixed submodules listing under file tab
-  - Fork feature (Angus MacArthur)
-  - git version check in gitlab:check
-  - Shared deploy keys feature
-  - Ability to generate default labels set for issues
-  - Improve gfm autocomplete (Harold Luo)
-  - Added support for Google Analytics
-  - Code search feature (Javier Castro)
-
-v 5.1.0
-  - You can login with email or username now
-  - Corrected project transfer rollback when repository cannot be moved
-  - Move both repo and wiki when project transfer requested
-  - Admin area: project editing was removed from admin namespace
-  - Access: admin user has now access to any project.
-  - Notification settings
-  - Gitlab::Git set of objects to abstract from grit library
-  - Replace Unicorn web server with Puma
-  - Backup/Restore refactored. Backup dump project wiki too now
-  - Restyled Issues list. Show milestone version in issue row
-  - Restyled Merge Request list
-  - Backup now dump/restore uploads
-  - Improved performance of dashboard (Andrew Kumanyaev)
-  - File history now tracks renames (Akzhan Abdulin)
-  - Drop wiki migration tools
-  - Drop sqlite migration tools
-  - project tagging
-  - Paginate users in API
-  - Restyled network graph (Hiroyuki Sato)
-
-v 5.0.1
-  - Fixed issue with gitlab-grit being overridden by grit
-
-v 5.0.0
-  - Replaced gitolite with gitlab-shell
-  - Removed gitolite-related libraries
-  - State machine added
-  - Setup gitlab as git user
-  - Internal API
-  - Show team tab for empty projects
-  - Import repository feature
-  - Updated rails
-  - Use lambda for scopes
-  - Redesign admin area -> users
-  - Redesign admin area -> user
-  - Secure link to file attachments
-  - Add validations for Group and Team names
-  - Restyle team page for project
-  - Update capybara, rspec-rails, poltergeist to recent versions
-  - Wiki on git using Gollum
-  - Added Solarized Dark theme for code review
-  - Don't show user emails in autocomplete lists, profile pages
-  - Added settings tab for group, team, project
-  - Replace user popup with icons in header
-  - Handle project moving with gitlab-shell
-  - Added select2-rails for selectboxes with ajax data load
-  - Fixed search field on projects page
-  - Added teams to search autocomplete
-  - Move groups and teams on dashboard sidebar to sub-tabs
-  - API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell)
-  - Redesign wall to be more like chat
-  - Snippets, Wall features are disabled by default for new projects
-
-v 4.2.0
-  - Teams
-  - User show page. Via /u/username
-  - Show help contents on pages for better navigation
-  - Async gitolite calls
-  - added satellites logs
-  - can_create_group, can_create_team booleans for User
-  - Process webhooks async
-  - GFM: Fix images escaped inside links
-  - Network graph improved
-  - Switchable branches for network graph
-  - API: Groups
-  - Fixed project download
-
-v 4.1.0
-  - Optional Sign-Up
-  - Discussions
-  - Satellites outside of tmp
-  - Line numbers for blame
-  - Project public mode
-  - Public area with unauthorized access
-  - Load dashboard events with ajax
-  - remember dashboard filter in cookies
-  - replace resque with sidekiq
-  - fix routing issues
-  - cleanup rake tasks
-  - fix backup/restore
-  - scss cleanup
-  - show preview for note images
-  - improved network-graph
-  - get rid of app/roles/
-  - added new classes Team, Repository
-  - Reduce amount of gitolite calls
-  - Ability to add user in all group projects
-  - remove deprecated configs
-  - replaced Korolev font with open font
-  - restyled admin/dashboard page
-  - restyled admin/projects page
-
-v 4.0.0
-  - Remove project code and path from API. Use id instead
-  - Return valid cloneable url to repo for webhook
-  - Fixed backup issue
-  - Reorganized settings
-  - Fixed commits compare
-  - Refactored scss
-  - Improve status checks
-  - Validates presence of User#name
-  - Fixed postgres support
-  - Removed sqlite support
-  - Modified post-receive hook
-  - Milestones can be closed now
-  - Show comment events on dashboard
-  - Quick add team members via group#people page
-  - [API] expose created date for hooks and SSH keys
-  - [API] list, create issue notes
-  - [API] list, create snippet notes
-  - [API] list, create wall notes
-  - Remove project code - use path instead
-  - added username field to user
-  - rake task to fill usernames based on emails create namespaces for users
-  - STI Group < Namespace
-  - Project has namespace_id
-  - Projects with namespaces also namespaced in gitolite and stored in subdir
-  - Moving project to group will move it under group namespace
-  - Ability to move project from namespaces to another
-  - Fixes commit patches getting escaped (see #2036)
-  - Support diff and patch generation for commits and merge request
-  - MergeReqest doesn't generate a temporary file for the patch any more
-  - Update the UI to allow downloading Patch or Diff
-
-v 3.1.0
-  - Updated gems
-  - Services: Gitlab CI integration
-  - Events filter on dashboard
-  - Own namespace for redis/resque
-  - Optimized commit diff views
-  - add alphabetical order for projects admin page
-  - Improved web editor
-  - Commit stats page
-  - Documentation split and cleanup
-  - Link to commit authors everywhere
-  - Restyled milestones list
-  - added Milestone to Merge Request
-  - Restyled Top panel
-  - Refactored Satellite Code
-  - Added file line links
-  - moved from capybara-webkit to poltergeist + phantomjs
-
-v 3.0.3
-  - Fixed bug with issues list in Chrome
-  - New Feature: Import team from another project
-
-v 3.0.2
-  - Fixed gitlab:app:setup
-  - Fixed application error on empty project in admin area
-  - Restyled last push widget
-
-v 3.0.1
-  - Fixed git over http
-
-v 3.0.0
-  - Projects groups
-  - Web Editor
-  - Fixed bug with gitolite keys
-  - UI improved
-  - Increased performance of application
-  - Show user avatar in last commit when browsing Files
-  - Refactored Gitlab::Merge
-  - Use Font Awesome for icons
-  - Separate observing of Note and MergeRequests
-  - Milestone "All Issues" filter
-  - Fix issue close and reopen button text and styles
-  - Fix forward/back while browsing Tree hierarchy
-  - Show number of notes for commits and merge requests
-  - Added support pg from box and update installation doc
-  - Reject ssh keys that break gitolite
-  - [API] list one project hook
-  - [API] edit project hook
-  - [API] list project snippets
-  - [API] allow to authorize using private token in HTTP header
-  - [API] add user creation
-
-v 2.9.1
-  - Fixed resque custom config init
-
-v 2.9.0
-  - fixed inline notes bugs
-  - refactored rspecs
-  - refactored gitolite backend
-  - added factory_girl
-  - restyled projects list on dashboard
-  - ssh keys validation to prevent gitolite crash
-  - send notifications if changed permission in project
-  - scss refactoring. gitlab_bootstrap/ dir
-  - fix git push http body bigger than 112k problem
-  - list of labels  page under issues tab
-  - API for milestones, keys
-  - restyled buttons
-  - OAuth
-  - Comment order changed
-
-v 2.8.1
-  - ability to disable gravatars
-  - improved MR diff logic
-  - ssh key help page
-
-v 2.8.0
-  - Gitlab Flavored Markdown
-  - Bulk issues update
-  - Issues API
-  - Cucumber coverage increased
-  - Post-receive files fixed
-  - UI improved
-  - Application cleanup
-  - more cucumber
-  - capybara-webkit + headless
-
-v 2.7.0
-  - Issue Labels
-  - Inline diff
-  - Git HTTP
-  - API
-  - UI improved
-  - System hooks
-  - UI improved
-  - Dashboard events endless scroll
-  - Source performance increased
-
-v 2.6.0
-  - UI polished
-  - Improved network graph + keyboard nav
-  - Handle huge commits
-  - Last Push widget
-  - Bugfix
-  - Better performance
-  - Email in resque
-  - Increased test coverage
-  - Ability to remove branch with MR accept
-  - a lot of code refactored
-
-v 2.5.0
-  - UI polished
-  - Git blame for file
-  - Bugfix
-  - Email in resque
-  - Better test coverage
-
-v 2.4.0
-  - Admin area stats page
-  - Ability to block user
-  - Simplified dashboard area
-  - Improved admin area
-  - Bootstrap 2.0
-  - Responsive layout
-  - Big commits handling
-  - Performance improved
-  - Milestones
-
-v 2.3.1
-  - Issues pagination
-  - ssl fixes
-  - Merge Request pagination
-
-v 2.3.0
-  - Dashboard r1
-  - Search r1
-  - Project page
-  - Close merge request on push
-  - Persist MR diff after merge
-  - mysql support
-  - Documentation
-
-v 2.2.0
-  - We’ve added support of LDAP auth
-  - Improved permission logic (4 roles system)
-  - Protected branches (now only masters can push to protected branches)
-  - Usability improved
-  - twitter bootstrap integrated
-  - compare view between commits
-  - wiki feature
-  - now you can enable/disable issues, wiki, wall features per project
-  - security fixes
-  - improved code browsing (ajax branch switch etc)
-  - improved per-line commenting
-  - git submodules displayed
-  - moved to rails 3.2
-  - help section improved
-
-v 2.1.0
-  - Project tab r1
-  - List branches/tags
-  - per line comments
-  - mass user import
-
-v 2.0.0
-  - gitolite as main git host system
-  - merge requests
-  - project/repo access
-  - link to commit/issue feed
-  - design tab
-  - improved email notifications
-  - restyled dashboard
-  - bugfix
-
-v 1.2.2
-  - common config file gitlab.yml
-  - issues restyle
-  - snippets restyle
-  - clickable news feed header on dashboard
-  - bugfix
-
-v 1.2.1
-  - bugfix
-
-v 1.2.0
-  - new design
-  - user dashboard
-  - network graph
-  - markdown support for comments
-  - encoding issues
-  - wall like twitter timeline
-
-v 1.1.0
-  - project dashboard
-  - wall redesigned
-  - feature: code snippets
-  - fixed horizontal scroll on file preview
-  - fixed app crash if commit message has invalid chars
-  - bugfix & code cleaning
-
-v 1.0.2
-  - fixed bug with empty project
-  - added adv validation for project path & code
-  - feature: issues can be sortable
-  - bugfix
-  - username displayed on top panel
-
-v 1.0.1
-  - fixed: with invalid source code for commit
-  - fixed: lose branch/tag selection when use tree navigation
-  - when history clicked - display path
-  - bug fix & code cleaning
-
-v 1.0.0
-  - bug fix
-  - projects preview mode
-
-v 0.9.6
-  - css fix
-  - new repo empty tree until restart server - fixed
-
-v 0.9.4
-  - security improved
-  - authorization improved
-  - html escaping
-  - bug fix
-  - increased test coverage
-  - design improvements
-
-v 0.9.1
-  - increased test coverage
-  - design improvements
-  - new issue email notification
-  - updated app name
-  - issue redesigned
-  - issue can be edit
-
-v 0.8.0
-  - syntax highlight for main file types
-  - redesign
-  - stability
-  - security fixes
-  - increased test coverage
-  - email notification
+## 7.14.3 through 0.8.0
+
+- See [changelogs/archive.md](changelogs/archive.md)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fbc8e15bebfbd1cf468dec8784777c9e06afb34c..67c30c2424c6360204ecd394168e38040562a08a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,7 +19,6 @@
         - [Technical debt](#technical-debt)
     - [Merge requests](#merge-requests)
         - [Merge request guidelines](#merge-request-guidelines)
-        - [Merge request description format](#merge-request-description-format)
         - [Contribution acceptance criteria](#contribution-acceptance-criteria)
     - [Changes for Stable Releases](#changes-for-stable-releases)
     - [Definition of done](#definition-of-done)
@@ -91,19 +90,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
 
 ## Implement design & UI elements
 
-### Design reference
-
-The GitLab design reference can be found in the [gitlab-design] project.
-The designs are made using Antetype (`.atype` files). You can use the
-[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
-(the PNG is 1:1).
-
-The current designs can be found in the [`gitlab8.atype` file].
-
-### UI development kit
-
-Implemented UI elements can also be found at https://gitlab.com/help/ui. Please
-note that this page isn't comprehensive at this time.
+Please see the [UI Guide for building GitLab].
 
 ## Issue tracker
 
@@ -129,7 +116,7 @@ request that potentially fixes it.
 
 ### Feature proposals
 
-To create a feature proposal for CE and CI, open an issue on the
+To create a feature proposal for CE, open an issue on the
 [issue tracker of CE][ce-tracker].
 
 For feature proposals for EE, open an issue on the
@@ -144,16 +131,7 @@ code snippet right after your description in a new line: `~"feature proposal"`.
 Please keep feature proposals as small and simple as possible, complex ones
 might be edited to make them small and simple.
 
-You are encouraged to use the template below for feature proposals.
-
-```
-## Description
-Include problem, use cases, benefits, and/or goals
-
-## Proposal
-
-## Links / references
-```
+Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
 
 For changes in the interface, it can be helpful to create a mockup first.
 If you want to create something yourself, consider opening an issue first to
@@ -166,55 +144,11 @@ submitting your own, there's a good chance somebody else had the same issue or
 feature proposal. Show your support with an award emoji and/or join the
 discussion.
 
-Please submit bugs using the following template in the issue description area.
+Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
 The text in the parenthesis is there to help you with what to include. Omit it
 when submitting the actual issue. You can copy-paste it and then edit as you
 see fit.
 
-```
-## Summary
-
-(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
-
-## Steps to reproduce
-
-(How one can reproduce the issue - this is very important)
-
-## Expected behavior
-
-(What you should see instead)
-
-## Relevant logs and/or screenshots
-
-(Paste any relevant logs - please use code blocks (```) to format console output,
-logs, and code as it's very hard to read otherwise.)
-
-## Output of checks
-
-### Results of GitLab Application Check
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:check SANITIZE=true)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
-
-(we will only investigate if the tests are passing)
-
-### Results of GitLab Environment Info
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:env:info)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
-
-## Possible fixes
-
-(If you can, link to the line of code that might be responsible for the problem)
-
-```
-
 ### Issue weight
 
 Issue weight allows us to get an idea of the amount of work required to solve
@@ -291,8 +225,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
 to be marked as `Accepting merge requests`. Please include screenshots or
 wireframes if the feature will also change the UI.
 
-Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
-[github.com][github-mr-tracker].
+Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
 
 If you are new to GitLab development (or web development in general), see the
 [I want to contribute!](#i-want-to-contribute) section to get you started with
@@ -311,19 +244,23 @@ tests are least likely to receive timely feedback. The workflow to make a merge
 request is as follows:
 
 1. Fork the project into your personal space on GitLab.com
-1. Create a feature branch, branch away from `master`.
+1. Create a feature branch, branch away from `master`
 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
-1. Add your changes to the [CHANGELOG](CHANGELOG)
-1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
+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. Push the commit(s) to your fork
 1. Submit a merge request (MR) to the `master` branch
 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, see the [merge request description format]
-   (#merge-request-description-format)
-1. If the MR changes the UI it should include before and after screenshots
+   used to achieve it.
+  1. If you are contributing code, fill in the template already provided in the
+     "Description" field.
+  1. If you are contributing documentation, choose `Documentation` from the
+     "Choose a template" menu and fill in the template.
+1. If the MR changes the UI it should include *Before* and *After* screenshots
 1. If the MR changes CSS classes please include the list of affected pages,
    `grep css-class ./app -R`
 1. Link any relevant [issues][ce-tracker] in the merge request description and
@@ -335,11 +272,17 @@ request is as follows:
    [shell command guidelines](doc/development/shell_commands.md)
 1. If your code creates new files on disk please read the
    [shared files guidelines](doc/development/shared_files.md).
-1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
+1. When writing commit messages please follow
+   [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+   [guidelines](http://chris.beams.io/posts/git-commit/).
 1. If your merge request adds one or more migrations, make sure to execute all
    migrations on a fresh database before the MR is reviewed. If the review leads
    to large changes in the MR, do this again once the review is complete.
 1. For more complex migrations, write tests.
+1. Merge requests **must** adhere to the [merge request performance
+   guidelines](doc/development/merge_request_performance_guidelines.md).
+1. For tests that use Capybara or PhantomJS, see this [article on how
+   to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
 
 The **official merge window** is in the beginning of the month from the 1st to
 the 7th day of the month. This is the best time to submit an MR and get
@@ -366,35 +309,19 @@ 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.
 
-### Merge request description format
-
-Please submit merge requests using the following template in the merge request
-description area. Copy-paste it to retain the markdown format.
-
-```
-## What does this MR do?
-
-## Are there points in the code the reviewer needs to double check?
-
-## Why was this MR needed?
-
-## What are the relevant issue numbers?
-
-## Screenshots (if relevant)
-```
-
 ### Contribution acceptance criteria
 
 1. The change is as small as possible
 1. Include proper tests and make all tests pass (unless it contains a test
-   exposing a bug in existing code)
+   exposing a bug in existing code). Every new class should have corresponding
+   unit tests, even if the class is exercised at a higher level, such as a feature test.
 1. If you suspect a failing CI build is unrelated to your contribution, you may
    try and restart the failing CI job or ask a developer to fix the
    aforementioned failing test
 1. Your MR initially contains a single commit (please use `git rebase -i` to
    squash commits)
-1. Your changes can merge without problems (if not please merge `master`, never
-   rebase commits pushed to the remote server)
+1. Your changes can merge without problems (if not please rebase if you're the
+   only one working on your feature branch, otherwise, merge `master`)
 1. Does not break any existing functionality
 1. Fixes one specific issue or implements one specific feature (do not combine
    things, send separate merge requests if needed)
@@ -412,7 +339,10 @@ description area. Copy-paste it to retain the markdown format.
       entire line to follow it. This prevents linting tools from generating warnings.
     - Don't touch neighbouring lines. As an exception, automatic mass
       refactoring modifications may leave style non-compliant.
-1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
+1. If the merge request adds any new libraries (gems, JavaScript libraries,
+   etc.), they should conform to our [Licensing guidelines][license-finder-doc].
+   See the instructions in that document for help if your MR fails the
+   "license-finder" test with a "Dependencies that need approval" error.
 
 ## Changes for Stable Releases
 
@@ -528,7 +458,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
 [accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
 [accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
 [gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
-[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
 [gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
 [git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
 [closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
@@ -536,10 +465,9 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
 [contributor-covenant]: http://contributor-covenant.org
 [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
 [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
+[changelog]: doc/development/changelog.md "Generate a changelog entry"
 [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
 [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
 [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
-[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
-[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
+[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
 [license-finder-doc]: doc/development/licensing.md
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 619b537668489eba5ff985e81afa2c1228281818..fcdb2e109f68cff5600955a73908885fe8599bb4 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.3.3
+4.0.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index e7c7d3cc3c89ada8384d34fee65534801993b979..3eefcb9dd5b38e2c1dc061052455dd97bcd51e6c 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.8
+1.0.0
diff --git a/Gemfile b/Gemfile
index 8b44b54e22c0cacaa9d80462026bc194ff64bc06..cb2a847012651b85f6605577ff72e78d5f28c47b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,10 +6,8 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
 # Responders respond_to and respond_with
 gem 'responders', '~> 2.0'
 
-# Specify a sprockets version due to increased performance
-# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
-gem 'sprockets', '~> 3.6.0'
-gem 'sprockets-es6'
+gem 'sprockets', '~> 3.7.0'
+gem 'sprockets-es6', '~> 0.9.2'
 
 # Default values for AR models
 gem 'default_value_for', '~> 3.0.0'
@@ -19,19 +17,19 @@ gem 'mysql2', '~> 0.3.16', group: :mysql
 gem 'pg', '~> 0.18.2', group: :postgres
 
 # Authentication libraries
-gem 'devise',                 '~> 4.0'
-gem 'doorkeeper',             '~> 4.0'
+gem 'devise',                 '~> 4.2'
+gem 'doorkeeper',             '~> 4.2.0'
 gem 'omniauth',               '~> 1.3.1'
 gem 'omniauth-auth0',         '~> 1.4.1'
 gem 'omniauth-azure-oauth2',  '~> 0.0.6'
 gem 'omniauth-bitbucket',     '~> 0.0.2'
 gem 'omniauth-cas3',          '~> 1.1.2'
-gem 'omniauth-facebook',      '~> 3.0.0'
+gem 'omniauth-facebook',      '~> 4.0.0'
 gem 'omniauth-github',        '~> 1.1.1'
-gem 'omniauth-gitlab',        '~> 1.0.0'
+gem 'omniauth-gitlab',        '~> 1.0.2'
 gem 'omniauth-google-oauth2', '~> 0.4.1'
 gem 'omniauth-kerberos',      '~> 0.3.0', group: :kerberos
-gem 'omniauth-saml',          '~> 1.6.0'
+gem 'omniauth-saml',          '~> 1.7.0'
 gem 'omniauth-shibboleth',    '~> 1.2.0'
 gem 'omniauth-twitter',       '~> 1.2.0'
 gem 'omniauth_crowd',         '~> 2.2.0'
@@ -53,7 +51,7 @@ gem 'browser', '~> 2.2'
 
 # Extracting information from a git repository
 # Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.4.5'
+gem 'gitlab_git', '~> 10.7.0'
 
 # LDAP Auth
 # GitLab fork with several improvements to original library. For full list of changes
@@ -77,7 +75,7 @@ gem 'rack-cors',    '~> 0.4.0', require: 'rack/cors'
 gem 'kaminari', '~> 0.17.0'
 
 # HAML
-gem 'hamlit', '~> 2.5'
+gem 'hamlit', '~> 2.6.1'
 
 # Files attachments
 gem 'carrierwave', '~> 0.10.0'
@@ -97,36 +95,34 @@ gem 'fog-rackspace', '~> 0.1.1'
 # for aws storage
 gem 'unf', '~> 0.1.4'
 
-# Authorization
-gem 'six', '~> 0.2.0'
-
 # Seed data
 gem 'seed-fu', '~> 2.3.5'
 
 # Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'task_list',     '~> 1.0.2', require: 'task_list/railtie'
-gem 'github-markup', '~> 1.4'
-gem 'redcarpet',     '~> 3.3.3'
-gem 'RedCloth',      '~> 4.3.2'
-gem 'rdoc',          '~>3.6'
-gem 'org-ruby',      '~> 0.9.12'
-gem 'creole',        '~> 0.5.0'
-gem 'wikicloth',     '0.8.1'
-gem 'asciidoctor',   '~> 1.5.2'
-gem 'rouge',         '~> 2.0'
+gem 'html-pipeline',      '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup',      '~> 1.5.0'
+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 '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
 gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
 
 # Diffs
-gem 'diffy', '~> 3.0.3'
+gem 'diffy', '~> 3.1.0'
 
 # Application server
 group :unicorn do
-  gem 'unicorn', '~> 4.9.0'
-  gem 'unicorn-worker-killer', '~> 0.4.2'
+  gem 'unicorn', '~> 5.1.0'
+  gem 'unicorn-worker-killer', '~> 0.4.4'
 end
 
 # State machine
@@ -135,11 +131,10 @@ gem 'state_machines-activerecord', '~> 0.4.0'
 gem 'after_commit_queue', '~> 1.3.0'
 
 # Issue tags
-gem 'acts-as-taggable-on', '~> 3.4'
+gem 'acts-as-taggable-on', '~> 4.0'
 
 # Background jobs
-gem 'sinatra', '~> 1.4.4', require: false
-gem 'sidekiq', '~> 4.0'
+gem 'sidekiq', '~> 4.2'
 gem 'sidekiq-cron', '~> 0.4.0'
 gem 'redis-namespace', '~> 1.5.2'
 
@@ -157,7 +152,7 @@ gem 'settingslogic', '~> 2.0.9'
 gem 'version_sorter', '~> 2.1.0'
 
 # Cache
-gem 'redis-rails', '~> 4.0.0'
+gem 'redis-rails', '~> 5.0.1'
 
 # Redis
 gem 'redis', '~> 3.2'
@@ -166,6 +161,9 @@ gem 'connection_pool', '~> 2.0'
 # HipChat integration
 gem 'hipchat', '~> 1.5.0'
 
+# JIRA integration
+gem 'jira-ruby', '~> 1.1.2'
+
 # Flowdock integration
 gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
 
@@ -198,10 +196,10 @@ gem 'loofah', '~> 2.0.3'
 gem 'licensee', '~> 8.0.0'
 
 # Protect against bruteforcing
-gem 'rack-attack', '~> 4.3.1'
+gem 'rack-attack', '~> 4.4.1'
 
 # Ace editor
-gem 'ace-rails-ap', '~> 4.0.2'
+gem 'ace-rails-ap', '~> 4.1.0'
 
 # Keyboard shortcuts
 gem 'mousetrap-rails', '~> 1.4.6'
@@ -209,10 +207,14 @@ gem 'mousetrap-rails', '~> 1.4.6'
 # Detect and convert string character encoding
 gem 'charlock_holmes', '~> 0.7.3'
 
-# Parse duration
+# Faster JSON
+gem 'oj', '~> 2.17.4'
+
+# Parse time & duration
+gem 'chronic', '~> 0.10.2'
 gem 'chronic_duration', '~> 0.10.6'
 
-gem 'sass-rails', '~> 5.0.0'
+gem 'sass-rails', '~> 5.0.6'
 gem 'coffee-rails', '~> 4.1.0'
 gem 'uglifier', '~> 2.7.2'
 gem 'turbolinks', '~> 2.5.0'
@@ -226,14 +228,14 @@ 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.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', '~> 1.1.0'
+gem 'sentry-raven', '~> 2.0.0'
 
 gem 'premailer-rails', '~> 1.9.0'
 
@@ -258,9 +260,6 @@ group :development do
   gem 'better_errors', '~> 1.0.1'
   gem 'binding_of_caller', '~> 0.7.2'
 
-  # Docs generator
-  gem 'sdoc', '~> 0.3.20'
-
   # thin instead webrick
   gem 'thin', '~> 1.7.0'
 end
@@ -297,11 +296,11 @@ group :development, :test do
   gem 'spring-commands-spinach',  '~> 1.1.0'
   gem 'spring-commands-teaspoon', '~> 0.0.2'
 
-  gem 'rubocop', '~> 0.41.2', require: false
+  gem 'rubocop', '~> 0.43.0', require: false
   gem 'rubocop-rspec', '~> 1.5.0', require: false
   gem 'scss_lint', '~> 0.47.0', require: false
+  gem 'haml_lint', '~> 0.18.2', require: false
   gem 'simplecov', '0.12.0', require: false
-  gem 'flog', '~> 4.3.2', require: false
   gem 'flay', '~> 2.6.1', require: false
   gem 'bundler-audit', '~> 0.5.0', require: false
 
@@ -309,29 +308,29 @@ group :development, :test do
 
   gem 'license_finder', '~> 2.1.0', require: false
   gem 'knapsack', '~> 1.11.0'
+
+  gem 'activerecord_sane_schema_dumper', '0.2'
 end
 
 group :test do
   gem 'shoulda-matchers', '~> 2.8.0', require: false
   gem 'email_spec', '~> 1.6.0'
+  gem 'json-schema', '~> 2.6.2'
   gem 'webmock', '~> 1.21.0'
   gem 'test_after_commit', '~> 0.4.2'
   gem 'sham_rack', '~> 1.3.6'
-end
-
-group :production do
-  gem 'gitlab_meta', '7.0'
+  gem 'timecop', '~> 0.8.0'
 end
 
 gem 'newrelic_rpm', '~> 3.16'
 
 gem 'octokit', '~> 4.3.0'
 
-gem 'mail_room', '~> 0.8'
+gem 'mail_room', '~> 0.9.0'
 
 gem 'email_reply_parser', '~> 0.5.8'
 
-gem 'ruby-prof', '~> 0.15.9'
+gem 'ruby-prof', '~> 0.16.2'
 
 ## CI
 gem 'activerecord-session_store', '~> 1.0.0'
@@ -344,8 +343,8 @@ gem 'oauth2', '~> 1.2.0'
 gem 'paranoia', '~> 2.0'
 
 # Health check
-gem 'health_check', '~> 2.1.0'
+gem 'health_check', '~> 2.2.0'
 
 # System information
-gem 'vmstat', '~> 2.1.1'
+gem 'vmstat', '~> 2.2'
 gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2244c20203b966dcafc6e066fea11e64cc1ba93c..290e4c3e1b32fb6b04827ea6290933211b1e19c3 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.0.2)
+    ace-rails-ap (4.1.0)
     actionmailer (4.2.7.1)
       actionpack (= 4.2.7.1)
       actionview (= 4.2.7.1)
@@ -38,14 +38,16 @@ GEM
       multi_json (~> 1.11, >= 1.11.2)
       rack (>= 1.5.2, < 3)
       railties (>= 4.0, < 5.1)
+    activerecord_sane_schema_dumper (0.2)
+      rails (>= 4, < 5)
     activesupport (4.2.7.1)
       i18n (~> 0.7)
       json (~> 1.7, >= 1.7.7)
       minitest (~> 5.1)
       thread_safe (~> 0.3, >= 0.3.4)
       tzinfo (~> 1.1)
-    acts-as-taggable-on (3.5.0)
-      activerecord (>= 3.2, < 5)
+    acts-as-taggable-on (4.0.0)
+      activerecord (>= 4.0)
     addressable (2.3.8)
     after_commit_queue (1.3.0)
       activerecord (>= 3.0)
@@ -128,6 +130,7 @@ GEM
       mime-types (>= 1.16)
     cause (0.1)
     charlock_holmes (0.7.3)
+    chronic (0.10.2)
     chronic_duration (0.10.6)
       numerizer (~> 0.1.1)
     chunky_png (1.3.5)
@@ -156,11 +159,15 @@ GEM
     database_cleaner (1.5.3)
     debug_inspector (0.0.2)
     debugger-ruby_core_source (1.3.8)
+    deckar01-task_list (1.0.6)
+      activesupport (~> 4.0)
+      html-pipeline
+      rack (~> 1.0)
     default_value_for (3.0.2)
       activerecord (>= 3.2.0, < 5.1)
     descendants_tracker (0.0.4)
       thread_safe (~> 0.3, >= 0.3.1)
-    devise (4.1.1)
+    devise (4.2.0)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
       railties (>= 4.1.0, < 5.1)
@@ -173,9 +180,9 @@ GEM
       railties
       rotp (~> 2.0)
     diff-lcs (1.2.5)
-    diffy (3.0.7)
+    diffy (3.1.0)
     docile (1.1.5)
-    doorkeeper (4.0.0)
+    doorkeeper (4.2.0)
       railties (>= 4.2)
     dropzonejs-rails (0.7.2)
       rails (> 3.1)
@@ -188,7 +195,7 @@ GEM
     erubis (2.7.0)
     escape_utils (1.1.1)
     eventmachine (1.0.8)
-    excon (0.49.0)
+    excon (0.52.0)
     execjs (2.6.0)
     expression_parser (0.9.0)
     factory_girl (4.5.0)
@@ -208,14 +215,11 @@ GEM
     flay (2.6.1)
       ruby_parser (~> 3.0)
       sexp_processor (~> 4.0)
-    flog (4.3.2)
-      ruby_parser (~> 3.1, > 3.1.0)
-      sexp_processor (~> 4.4)
     flowdock (0.7.1)
       httparty (~> 0.7)
       multi_json
-    fog-aws (0.9.2)
-      fog-core (~> 1.27)
+    fog-aws (0.11.0)
+      fog-core (~> 1.38)
       fog-json (~> 1.0)
       fog-xml (~> 0.1)
       ipaddress (~> 0.8)
@@ -224,7 +228,7 @@ GEM
       fog-core (~> 1.27)
       fog-json (~> 1.0)
       fog-xml (~> 0.1)
-    fog-core (1.40.0)
+    fog-core (1.42.0)
       builder
       excon (~> 0.49)
       formatador (~> 0.2)
@@ -278,12 +282,12 @@ GEM
       diff-lcs (~> 1.1)
       mime-types (>= 1.16, < 3)
       posix-spawn (~> 0.3)
-    gitlab_git (10.4.5)
+    gitlab-markup (1.5.0)
+    gitlab_git (10.7.0)
       activesupport (~> 4.0)
       charlock_holmes (~> 0.7.3)
       github-linguist (~> 4.7.0)
       rugged (~> 0.24.0)
-    gitlab_meta (7.0)
     gitlab_omniauth-ldap (1.2.1)
       net-ldap (~> 0.9)
       omniauth (~> 1.0)
@@ -321,12 +325,19 @@ GEM
     grape-entity (0.4.8)
       activesupport
       multi_json (>= 1.3.2)
-    hamlit (2.5.0)
+    haml (4.0.7)
+      tilt
+    haml_lint (0.18.2)
+      haml (~> 4.0)
+      rake (>= 10, < 12)
+      rubocop (>= 0.36.0)
+      sysexits (~> 1.1)
+    hamlit (2.6.1)
       temple (~> 0.7.6)
       thor
       tilt
-    hashie (3.4.3)
-    health_check (2.1.0)
+    hashie (3.4.4)
+    health_check (2.2.1)
       rails (>= 4.0)
     hipchat (1.5.2)
       httparty
@@ -345,6 +356,9 @@ GEM
       cause
       json
     ipaddress (0.8.3)
+    jira-ruby (1.1.2)
+      activesupport
+      oauth (~> 0.5, >= 0.5.0)
     jquery-atwho-rails (1.3.2)
     jquery-rails (4.1.1)
       rails-dom-testing (>= 1, < 3)
@@ -356,6 +370,8 @@ GEM
     jquery-ui-rails (5.0.5)
       railties (>= 3.2.16)
     json (1.8.3)
+    json-schema (2.6.2)
+      addressable (~> 2.3.8)
     jwt (1.5.4)
     kaminari (0.17.0)
       actionpack (>= 3.0.0)
@@ -389,9 +405,9 @@ GEM
       systemu (~> 2.6.2)
     mail (2.6.4)
       mime-types (>= 1.16, < 4)
-    mail_room (0.8.0)
+    mail_room (0.9.0)
     method_source (0.8.2)
-    mime-types (2.99.2)
+    mime-types (2.99.3)
     mimemagic (0.3.0)
     mini_portile2 (2.1.0)
     minitest (5.7.0)
@@ -408,7 +424,7 @@ GEM
       mini_portile2 (~> 2.1.0)
       pkg-config (~> 1.1.7)
     numerizer (0.1.1)
-    oauth (0.4.7)
+    oauth (0.5.1)
     oauth2 (1.2.0)
       faraday (>= 0.8, < 0.10)
       jwt (~> 1.0)
@@ -417,6 +433,7 @@ GEM
       rack (>= 1.2, < 3)
     octokit (4.3.0)
       sawyer (~> 0.7.0, >= 0.5.3)
+    oj (2.17.4)
     omniauth (1.3.1)
       hashie (>= 1.2, < 4)
       rack (>= 1.0, < 3)
@@ -434,12 +451,12 @@ GEM
       addressable (~> 2.3)
       nokogiri (~> 1.6.6)
       omniauth (~> 1.2)
-    omniauth-facebook (3.0.0)
+    omniauth-facebook (4.0.0)
       omniauth-oauth2 (~> 1.2)
     omniauth-github (1.1.2)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.1)
-    omniauth-gitlab (1.0.1)
+    omniauth-gitlab (1.0.2)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.0)
     omniauth-google-oauth2 (0.4.1)
@@ -459,9 +476,9 @@ GEM
     omniauth-oauth2 (1.3.1)
       oauth2 (~> 1.0)
       omniauth (~> 1.2)
-    omniauth-saml (1.6.0)
+    omniauth-saml (1.7.0)
       omniauth (~> 1.3)
-      ruby-saml (~> 1.3)
+      ruby-saml (~> 1.4)
     omniauth-shibboleth (1.2.1)
       omniauth (>= 1.0.0)
     omniauth-twitter (1.2.1)
@@ -476,7 +493,7 @@ GEM
     orm_adapter (0.5.0)
     paranoia (2.1.4)
       activerecord (~> 4.0)
-    parser (2.3.1.2)
+    parser (2.3.1.4)
       ast (~> 2.2)
     pg (0.18.4)
     pkg-config (1.1.7)
@@ -503,7 +520,7 @@ GEM
     rack (1.6.4)
     rack-accept (0.4.5)
       rack (>= 0.4)
-    rack-attack (4.3.1)
+    rack-attack (4.4.1)
       rack
     rack-cors (0.4.0)
     rack-mount (0.8.3)
@@ -543,45 +560,45 @@ GEM
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rainbow (2.1.0)
-    raindrops (0.15.0)
+    raindrops (0.17.0)
     rake (10.5.0)
     rb-fsevent (0.9.6)
     rb-inotify (0.9.5)
       ffi (>= 0.5.0)
     rblineprof (0.3.6)
       debugger-ruby_core_source (~> 1.3)
-    rdoc (3.12.2)
+    rdoc (4.2.2)
       json (~> 1.4)
     recaptcha (3.0.0)
       json
     redcarpet (3.3.3)
     redis (3.2.2)
-    redis-actionpack (4.0.1)
-      actionpack (~> 4)
-      redis-rack (~> 1.5.0)
-      redis-store (~> 1.1.0)
-    redis-activesupport (4.1.5)
-      activesupport (>= 3, < 5)
-      redis-store (~> 1.1.0)
+    redis-actionpack (5.0.1)
+      actionpack (>= 4.0, < 6)
+      redis-rack (>= 1, < 3)
+      redis-store (>= 1.1.0, < 1.4.0)
+    redis-activesupport (5.0.1)
+      activesupport (>= 3, < 6)
+      redis-store (~> 1.2.0)
     redis-namespace (1.5.2)
       redis (~> 3.0, >= 3.0.4)
-    redis-rack (1.5.0)
+    redis-rack (1.6.0)
       rack (~> 1.5)
-      redis-store (~> 1.1.0)
-    redis-rails (4.0.0)
-      redis-actionpack (~> 4)
-      redis-activesupport (~> 4)
-      redis-store (~> 1.1.0)
-    redis-store (1.1.7)
+      redis-store (~> 1.2.0)
+    redis-rails (5.0.1)
+      redis-actionpack (~> 5.0.0)
+      redis-activesupport (~> 5.0.0)
+      redis-store (~> 1.2.0)
+    redis-store (1.2.0)
       redis (>= 2.2)
     request_store (1.3.1)
     rerun (0.11.0)
       listen (~> 3.0)
-    responders (2.1.1)
+    responders (2.3.0)
       railties (>= 4.2.0, < 5.1)
     rinku (2.0.0)
     rotp (2.1.2)
-    rouge (2.0.5)
+    rouge (2.0.6)
     rqrcode (0.7.0)
       chunky_png
     rqrcode-rails3 (0.1.7)
@@ -609,7 +626,7 @@ GEM
     rspec-retry (0.4.5)
       rspec-core
     rspec-support (3.5.0)
-    rubocop (0.41.2)
+    rubocop (0.43.0)
       parser (>= 2.3.1.1, < 3.0)
       powerpack (~> 0.1)
       rainbow (>= 1.99.1, < 3.0)
@@ -619,9 +636,9 @@ GEM
       rubocop (>= 0.40.0)
     ruby-fogbugz (0.2.1)
       crack (~> 0.4)
-    ruby-prof (0.15.9)
+    ruby-prof (0.16.2)
     ruby-progressbar (1.8.1)
-    ruby-saml (1.3.0)
+    ruby-saml (1.4.1)
       nokogiri (>= 1.5.10)
     ruby_parser (3.8.2)
       sexp_processor (~> 4.1)
@@ -634,7 +651,7 @@ GEM
     sanitize (2.1.0)
       nokogiri (>= 1.4.4)
     sass (3.4.22)
-    sass-rails (5.0.5)
+    sass-rails (5.0.6)
       railties (>= 4.0.0, < 6)
       sass (~> 3.1)
       sprockets (>= 2.8, < 4.0)
@@ -646,27 +663,24 @@ GEM
     scss_lint (0.47.1)
       rake (>= 0.9, < 11)
       sass (~> 3.4.15)
-    sdoc (0.3.20)
-      json (>= 1.1.3)
-      rdoc (~> 3.10)
     seed-fu (2.3.6)
       activerecord (>= 3.1)
       activesupport (>= 3.1)
     select2-rails (3.5.9.3)
       thor (~> 0.14)
-    sentry-raven (1.1.0)
-      faraday (>= 0.7.6)
+    sentry-raven (2.0.2)
+      faraday (>= 0.7.6, < 0.10.x)
     settingslogic (2.0.9)
     sexp_processor (4.7.0)
     sham_rack (1.3.6)
       rack
     shoulda-matchers (2.8.0)
       activesupport (>= 3.0.0)
-    sidekiq (4.1.4)
+    sidekiq (4.2.1)
       concurrent-ruby (~> 1.0)
       connection_pool (~> 2.2, >= 2.2.0)
+      rack-protection (~> 1.5)
       redis (~> 3.2, >= 3.2.1)
-      sinatra (>= 1.4.7)
     sidekiq-cron (0.4.0)
       redis-namespace (>= 1.5.2)
       rufus-scheduler (>= 2.0.24)
@@ -676,11 +690,6 @@ GEM
       json (>= 1.8, < 3)
       simplecov-html (~> 0.10.0)
     simplecov-html (0.10.0)
-    sinatra (1.4.7)
-      rack (~> 1.5)
-      rack-protection (~> 1.4)
-      tilt (>= 1.3, < 3)
-    six (0.2.0)
     slack-notifier (1.2.1)
     slop (3.6.0)
     spinach (0.8.10)
@@ -700,10 +709,10 @@ GEM
       spring (>= 0.9.1)
     spring-commands-teaspoon (0.0.2)
       spring (>= 0.9.1)
-    sprockets (3.6.3)
+    sprockets (3.7.0)
       concurrent-ruby (~> 1.0)
       rack (> 1, < 3)
-    sprockets-es6 (0.9.0)
+    sprockets-es6 (0.9.2)
       babel-source (>= 5.8.11)
       babel-transpiler
       sprockets (>= 3.0.0)
@@ -721,9 +730,8 @@ GEM
     stringex (2.5.2)
     sys-filesystem (1.1.6)
       ffi
+    sysexits (1.2.0)
     systemu (2.6.5)
-    task_list (1.0.2)
-      html-pipeline
     teaspoon (1.1.5)
       railties (>= 3.2.5, < 6)
     teaspoon-jasmine (2.2.0)
@@ -740,6 +748,9 @@ GEM
     tilt (2.0.5)
     timecop (0.8.1)
     timfel-krb5-auth (0.8.3)
+    truncato (0.7.8)
+      htmlentities (~> 4.3.1)
+      nokogiri (~> 1.6.1)
     turbolinks (2.5.3)
       coffee-rails
     tzinfo (1.2.2)
@@ -752,10 +763,9 @@ GEM
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.2)
-    unicode-display_width (1.1.0)
-    unicorn (4.9.0)
+    unicode-display_width (1.1.1)
+    unicorn (5.1.0)
       kgio (~> 2.6)
-      rack
       raindrops (~> 0.7)
     unicorn-worker-killer (0.4.4)
       get_process_mem (~> 0)
@@ -769,7 +779,7 @@ GEM
       coercible (~> 1.0)
       descendants_tracker (~> 0.0, >= 0.0.3)
       equalizer (~> 0.0, >= 0.0.9)
-    vmstat (2.1.1)
+    vmstat (2.2.0)
     warden (1.2.6)
       rack (>= 1.0)
     web-console (2.3.0)
@@ -796,9 +806,10 @@ PLATFORMS
 
 DEPENDENCIES
   RedCloth (~> 4.3.2)
-  ace-rails-ap (~> 4.0.2)
+  ace-rails-ap (~> 4.1.0)
   activerecord-session_store (~> 1.0.0)
-  acts-as-taggable-on (~> 3.4)
+  activerecord_sane_schema_dumper (= 0.2)
+  acts-as-taggable-on (~> 4.0)
   addressable (~> 2.3.8)
   after_commit_queue (~> 1.3.0)
   akismet (~> 2.0)
@@ -822,24 +833,25 @@ DEPENDENCIES
   capybara-screenshot (~> 1.0.0)
   carrierwave (~> 0.10.0)
   charlock_holmes (~> 0.7.3)
+  chronic (~> 0.10.2)
   chronic_duration (~> 0.10.6)
   coffee-rails (~> 4.1.0)
   connection_pool (~> 2.0)
   creole (~> 0.5.0)
   d3_rails (~> 3.5.0)
   database_cleaner (~> 1.5.0)
+  deckar01-task_list (= 1.0.6)
   default_value_for (~> 3.0.0)
-  devise (~> 4.0)
+  devise (~> 4.2)
   devise-two-factor (~> 3.0.0)
-  diffy (~> 3.0.3)
-  doorkeeper (~> 4.0)
+  diffy (~> 3.1.0)
+  doorkeeper (~> 4.2.0)
   dropzonejs-rails (~> 0.7.1)
   email_reply_parser (~> 0.5.8)
   email_spec (~> 1.6.0)
   factory_girl_rails (~> 4.6.0)
   ffaker (~> 2.0.0)
   flay (~> 2.6.1)
-  flog (~> 4.3.2)
   fog-aws (~> 0.9)
   fog-azure (~> 0.0)
   fog-core (~> 1.40)
@@ -853,26 +865,28 @@ DEPENDENCIES
   gemnasium-gitlab-service (~> 0.2)
   gemojione (~> 3.0)
   github-linguist (~> 4.7.0)
-  github-markup (~> 1.4)
   gitlab-flowdock-git-hook (~> 1.0.1)
-  gitlab_git (~> 10.4.5)
-  gitlab_meta (= 7.0)
+  gitlab-markup (~> 1.5.0)
+  gitlab_git (~> 10.7.0)
   gitlab_omniauth-ldap (~> 1.2.1)
   gollum-lib (~> 4.2)
   gollum-rugged_adapter (~> 0.4.2)
   gon (~> 6.1.0)
   grape (~> 0.15.0)
   grape-entity (~> 0.4.2)
-  hamlit (~> 2.5)
-  health_check (~> 2.1.0)
+  haml_lint (~> 0.18.2)
+  hamlit (~> 2.6.1)
+  health_check (~> 2.2.0)
   hipchat (~> 1.5.0)
   html-pipeline (~> 1.11.0)
   httparty (~> 0.13.3)
   influxdb (~> 0.2)
+  jira-ruby (~> 1.1.2)
   jquery-atwho-rails (~> 1.3.2)
   jquery-rails (~> 4.1.0)
   jquery-turbolinks (~> 2.1.0)
   jquery-ui-rails (~> 5.0.0)
+  json-schema (~> 2.6.2)
   jwt
   kaminari (~> 0.17.0)
   knapsack (~> 1.11.0)
@@ -880,7 +894,7 @@ DEPENDENCIES
   license_finder (~> 2.1.0)
   licensee (~> 8.0.0)
   loofah (~> 2.0.3)
-  mail_room (~> 0.8)
+  mail_room (~> 0.9.0)
   method_source (~> 0.8)
   minitest (~> 5.7.0)
   mousetrap-rails (~> 1.4.6)
@@ -891,17 +905,18 @@ DEPENDENCIES
   nokogiri (~> 1.6.7, >= 1.6.7.2)
   oauth2 (~> 1.2.0)
   octokit (~> 4.3.0)
+  oj (~> 2.17.4)
   omniauth (~> 1.3.1)
   omniauth-auth0 (~> 1.4.1)
   omniauth-azure-oauth2 (~> 0.0.6)
   omniauth-bitbucket (~> 0.0.2)
   omniauth-cas3 (~> 1.1.2)
-  omniauth-facebook (~> 3.0.0)
+  omniauth-facebook (~> 4.0.0)
   omniauth-github (~> 1.1.1)
-  omniauth-gitlab (~> 1.0.0)
+  omniauth-gitlab (~> 1.0.2)
   omniauth-google-oauth2 (~> 0.4.1)
   omniauth-kerberos (~> 0.3.0)
-  omniauth-saml (~> 1.6.0)
+  omniauth-saml (~> 1.7.0)
   omniauth-shibboleth (~> 1.2.0)
   omniauth-twitter (~> 1.2.0)
   omniauth_crowd (~> 2.2.0)
@@ -911,45 +926,42 @@ DEPENDENCIES
   poltergeist (~> 1.9.0)
   premailer-rails (~> 1.9.0)
   pry-rails (~> 0.3.4)
-  rack-attack (~> 4.3.1)
+  rack-attack (~> 4.4.1)
   rack-cors (~> 0.4.0)
   rack-oauth2 (~> 1.2.1)
   rails (= 4.2.7.1)
   rails-deprecated_sanitizer (~> 1.0.3)
   rainbow (~> 2.1.0)
   rblineprof (~> 0.3.6)
-  rdoc (~> 3.6)
+  rdoc (~> 4.2)
   recaptcha (~> 3.0)
   redcarpet (~> 3.3.3)
   redis (~> 3.2)
   redis-namespace (~> 1.5.2)
-  redis-rails (~> 4.0.0)
-  request_store (~> 1.3.0)
+  redis-rails (~> 5.0.1)
+  request_store (~> 1.3)
   rerun (~> 0.11.0)
   responders (~> 2.0)
   rouge (~> 2.0)
   rqrcode-rails3 (~> 0.1.7)
   rspec-rails (~> 3.5.0)
   rspec-retry (~> 0.4.5)
-  rubocop (~> 0.41.2)
+  rubocop (~> 0.43.0)
   rubocop-rspec (~> 1.5.0)
   ruby-fogbugz (~> 0.2.1)
-  ruby-prof (~> 0.15.9)
+  ruby-prof (~> 0.16.2)
   sanitize (~> 2.0)
-  sass-rails (~> 5.0.0)
+  sass-rails (~> 5.0.6)
   scss_lint (~> 0.47.0)
-  sdoc (~> 0.3.20)
   seed-fu (~> 2.3.5)
   select2-rails (~> 3.5.9)
-  sentry-raven (~> 1.1.0)
+  sentry-raven (~> 2.0.0)
   settingslogic (~> 2.0.9)
   sham_rack (~> 1.3.6)
   shoulda-matchers (~> 2.8.0)
-  sidekiq (~> 4.0)
+  sidekiq (~> 4.2)
   sidekiq-cron (~> 0.4.0)
   simplecov (= 0.12.0)
-  sinatra (~> 1.4.4)
-  six (~> 0.2.0)
   slack-notifier (~> 1.2.0)
   spinach-rails (~> 0.2.1)
   spinach-rerun-reporter (~> 0.0.2)
@@ -957,28 +969,29 @@ DEPENDENCIES
   spring-commands-rspec (~> 1.0.4)
   spring-commands-spinach (~> 1.1.0)
   spring-commands-teaspoon (~> 0.0.2)
-  sprockets (~> 3.6.0)
-  sprockets-es6
+  sprockets (~> 3.7.0)
+  sprockets-es6 (~> 0.9.2)
   state_machines-activerecord (~> 0.4.0)
   sys-filesystem (~> 1.1.6)
-  task_list (~> 1.0.2)
   teaspoon (~> 1.1.0)
   teaspoon-jasmine (~> 2.2.0)
   test_after_commit (~> 0.4.2)
   thin (~> 1.7.0)
+  timecop (~> 0.8.0)
+  truncato (~> 0.7.8)
   turbolinks (~> 2.5.0)
   u2f (~> 0.2.1)
   uglifier (~> 2.7.2)
   underscore-rails (~> 1.8.0)
   unf (~> 0.1.4)
-  unicorn (~> 4.9.0)
-  unicorn-worker-killer (~> 0.4.2)
+  unicorn (~> 5.1.0)
+  unicorn-worker-killer (~> 0.4.4)
   version_sorter (~> 2.1.0)
   virtus (~> 1.0.1)
-  vmstat (~> 2.1.1)
+  vmstat (~> 2.2)
   web-console (~> 2.0)
   webmock (~> 1.21.0)
   wikicloth (= 0.8.1)
 
 BUNDLED WITH
-   1.12.5
+   1.13.6
diff --git a/PROCESS.md b/PROCESS.md
index 8e1a3f7360f6508b35cf76ea8703dad873c72b32..8af660fbdd156c163f0cbc06e76cc8bba6619e24 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -50,7 +50,7 @@ etc.).
 
 The most important thing is making sure valid issues receive feedback from the
 development team. Therefore the priority is mentioning developers that can help
-on those issue. Please select someone with relevant experience from
+on those issues. Please select someone with relevant experience from
 [GitLab core team][core-team]. If there is nobody mentioned with that expertise
 look in the commit history for the affected files to find someone. Avoid
 mentioning the lead developer, this is the person that is least likely to give a
diff --git a/README.md b/README.md
index fee93d5f9c304c61f6e1a863620cfc2e3ec86df5..dbe6db3ebed9a8ecd83f1656b18ea98dbe7cfe72 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
 # GitLab
 
-[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+[![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)](http://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)
 
 ## Canonical source
 
-The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
+The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
 
 ## Open source software to collaborate on code
 
@@ -54,6 +56,10 @@ There are various other options to install GitLab, please refer to the [installa
 
 You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password.
 
+## Contributing
+
+GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+
 ## Install a development environment
 
 To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
@@ -69,12 +75,12 @@ Instructions on how to start GitLab and how to run the tests can be found in the
 GitLab is a Ruby on Rails application that runs on the following software:
 
 - Ubuntu/Debian/CentOS/RHEL
-- Ruby (MRI) 2.1
+- Ruby (MRI) 2.3
 - Git 2.7.4+
 - Redis 2.8+
 - MySQL or PostgreSQL
 
-For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html).
+For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
 
 ## Third-party applications
 
@@ -90,7 +96,7 @@ For upgrading information please see our [update page](https://about.gitlab.com/
 
 ## Documentation
 
-All documentation can be found on [doc.gitlab.com/ce/](http://doc.gitlab.com/ce/).
+All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/ce/).
 
 ## Getting help
 
diff --git a/VERSION b/VERSION
index 542e7824102447bf9fe4091b802e0713848e87c4..919f462addc25d11511eef8a4bfd87f5d47a263d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.11.0-pre
+8.14.0-pre
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
deleted file mode 100644
index 5b55e12571c894c5f34d3cd0d5325919d1919e6f..0000000000000000000000000000000000000000
Binary files a/app/assets/images/icon-link.png and /dev/null differ
diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7e242586bad3d9d0db741d4a343e1c6793ea45c5
--- /dev/null
+++ b/app/assets/images/icon_anchor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg>
\ No newline at end of file
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ad89d684d94d63c7e0091b6c4ee215cf3e511bf1
--- /dev/null
+++ b/app/assets/images/koding-logo.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
+    <g fill="#d6d7d9">
+        <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
+        <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
+        <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3f4ef31947bc4a53d6b2d243d6e9f2c975c84b77
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
new file mode 100644
index 0000000000000000000000000000000000000000..387628f831c2ef2d14863a2e30bde7c6f3e5bd85
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
new file mode 100644
index 0000000000000000000000000000000000000000..5f8f8ca143c16c801220f5fc40be68a39c0e8cb3
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
new file mode 100644
index 0000000000000000000000000000000000000000..27a55b1d61fd88ef897d0021de7570288e5db0e6
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8fe3281d2f6dc2fe80c94290a2fe73c93bfdd06e
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4260e312929d8fa0e9aacda888f99c5d86bc75b0
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6de166ce0a2643e020fce46cedccfac3e078c5e3
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif differ
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
deleted file mode 100644
index 151455ce4a3a9f49c928593e864639e555d497cb..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/LabelManager.js
+++ /dev/null
@@ -1,110 +0,0 @@
-(function() {
-  this.LabelManager = (function() {
-    LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
-
-    function LabelManager(opts) {
-      var ref, ref1, ref2;
-      if (opts == null) {
-        opts = {};
-      }
-      this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
-      this.prioritizedLabels.sortable({
-        items: 'li',
-        placeholder: 'list-placeholder',
-        axis: 'y',
-        update: this.onPrioritySortUpdate.bind(this)
-      });
-      this.bindEvents();
-    }
-
-    LabelManager.prototype.bindEvents = function() {
-      return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
-    };
-
-    LabelManager.prototype.onTogglePriorityClick = function(e) {
-      var $btn, $label, $tooltip, _this, action;
-      e.preventDefault();
-      _this = e.data;
-      $btn = $(e.currentTarget);
-      $label = $("#" + ($btn.data('domId')));
-      action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
-      $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
-      $tooltip.tooltip('destroy');
-      return _this.toggleLabelPriority($label, action);
-    };
-
-    LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
-      var $from, $target, _this, url, xhr;
-      if (persistState == null) {
-        persistState = true;
-      }
-      _this = this;
-      url = $label.find('.js-toggle-priority').data('url');
-      $target = this.prioritizedLabels;
-      $from = this.otherLabels;
-      if (action === 'remove') {
-        $target = this.otherLabels;
-        $from = this.prioritizedLabels;
-      }
-      if ($from.find('li').length === 1) {
-        $from.find('.empty-message').removeClass('hidden');
-      }
-      if (!$target.find('li').length) {
-        $target.find('.empty-message').addClass('hidden');
-      }
-      $label.detach().appendTo($target);
-      if (!persistState) {
-        return;
-      }
-      if (action === 'remove') {
-        xhr = $.ajax({
-          url: url,
-          type: 'DELETE'
-        });
-        if (!$from.find('li').length) {
-          $from.find('.empty-message').removeClass('hidden');
-        }
-      } else {
-        xhr = this.savePrioritySort($label, action);
-      }
-      return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
-    };
-
-    LabelManager.prototype.onPrioritySortUpdate = function() {
-      var xhr;
-      xhr = this.savePrioritySort();
-      return xhr.fail(function() {
-        return new Flash(this.errorMessage, 'alert');
-      });
-    };
-
-    LabelManager.prototype.savePrioritySort = function() {
-      return $.post({
-        url: this.prioritizedLabels.data('url'),
-        data: {
-          label_ids: this.getSortedLabelsIds()
-        }
-      });
-    };
-
-    LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) {
-      var action;
-      action = originalAction === 'remove' ? 'add' : 'remove';
-      this.toggleLabelPriority($label, action, false);
-      return new Flash(this.errorMessage, 'alert');
-    };
-
-    LabelManager.prototype.getSortedLabelsIds = function() {
-      var sortedIds;
-      sortedIds = [];
-      this.prioritizedLabels.find('li').each(function() {
-        return sortedIds.push($(this).data('id'));
-      });
-      return sortedIds;
-    };
-
-    return LabelManager;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..82e526ae0ef44af8b276eddc133c7aa350e4cff7
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,39 @@
+/* eslint-disable */
+((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
index 1ab3c2197d82f60cfd44757db75e8dd340018b92..919107b8cb9497a8a4ec26a38351a588835a7e43 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Activities = (function() {
     function Activities() {
@@ -12,25 +13,21 @@
     }
 
     Activities.prototype.updateTooltips = function() {
-      return gl.utils.localTimeAgo($('.js-timeago', '#activity'));
+      gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
     };
 
     Activities.prototype.reloadActivities = function() {
       $(".content_list").html('');
-      return Pager.init(20, true);
+      Pager.init(20, true, false, this.updateTooltips);
     };
 
     Activities.prototype.toggleFilter = function(sender) {
-      var event_filters, filter;
+      var filter = sender.attr("id").split("_")[0];
+
       $('.event-filter .active').removeClass("active");
-      event_filters = $.cookie("event_filter");
-      filter = sender.attr("id").split("_")[0];
-      $.cookie("event_filter", (event_filters !== filter ? filter : ""), {
-        path: '/'
-      });
-      if (event_filters !== filter) {
-        return sender.closest('li').toggleClass("active");
-      }
+      Cookies.set("event_filter", filter);
+
+      sender.closest('li').toggleClass("active");
     };
 
     return Activities;
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index f8460beb5d27cdba9f5f532c1e5104d81f1aa840..1ef340e4ca11bf2c400a47c4910fc1208d85efcf 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Admin = (function() {
     function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84b292e59c643756ceefa00024be7529557b8a68..1cab66e109e57d7b44b15a797c4f77b23994920c 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Api = {
     groupsPath: "/api/:version/groups.json",
@@ -5,45 +6,41 @@
     namespacesPath: "/api/:version/namespaces.json",
     groupProjectsPath: "/api/:version/groups/:id/projects.json",
     projectsPath: "/api/:version/projects.json?simple=true",
-    labelsPath: "/api/:version/projects/:id/labels",
-    licensePath: "/api/:version/licenses/:key",
-    gitignorePath: "/api/:version/gitignores/:key",
-    gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+    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",
     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,
-        data: {
-          private_token: gon.api_token
-        },
         dataType: "json"
       }).done(function(group) {
         return callback(group);
       });
     },
-    groups: function(query, skip_ldap, callback) {
+    // Return groups list. Filtered by query
+    groups: function(query, options, callback) {
       var url = Api.buildUrl(Api.groupsPath);
       return $.ajax({
         url: url,
-        data: {
-          private_token: gon.api_token,
-          search: query,
-          per_page: 20
-        },
+        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: {
-          private_token: gon.api_token,
           search: query,
           per_page: 20
         },
@@ -52,12 +49,12 @@
         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: {
-          private_token: gon.api_token,
           search: query,
           order_by: order,
           per_page: 20
@@ -67,14 +64,14 @@
         return callback(projects);
       });
     },
-    newLabel: function(project_id, data, callback) {
+    newLabel: function(namespace_path, project_path, data, callback) {
       var url = Api.buildUrl(Api.labelsPath)
-        .replace(':id', project_id);
-      data.private_token = gon.api_token;
+        .replace(':namespace_path', namespace_path)
+        .replace(':project_path', project_path);
       return $.ajax({
         url: url,
         type: "POST",
-        data: data,
+        data: {'label': data},
         dataType: "json"
       }).done(function(label) {
         return callback(label);
@@ -82,13 +79,13 @@
         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: {
-          private_token: gon.api_token,
           search: query,
           per_page: 20
         },
@@ -97,6 +94,7 @@
         return callback(projects);
       });
     },
+    // Return text for a specific license
     licenseText: function(key, data, callback) {
       var url = Api.buildUrl(Api.licensePath)
         .replace(':key', key);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index e596b98603b6c081bfb10038cfcab735772688bf..33c1708e1a9f8683e3408e82ac39a737776c41d6 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,3 +1,10 @@
+/* eslint-disable */
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
 /*= require jquery2 */
 /*= require jquery-ui/autocomplete */
 /*= require jquery-ui/datepicker */
@@ -5,13 +12,13 @@
 /*= require jquery-ui/effect-highlight */
 /*= require jquery-ui/sortable */
 /*= require jquery_ujs */
-/*= require jquery.cookie */
 /*= require jquery.endless-scroll */
 /*= require jquery.highlight */
 /*= require jquery.waitforimages */
 /*= require jquery.atwho */
 /*= require jquery.scrollTo */
 /*= require jquery.turbolinks */
+/*= require js.cookie */
 /*= require turbolinks */
 /*= require autosave */
 /*= require bootstrap/affix */
@@ -26,8 +33,6 @@
 /*= require bootstrap/tooltip */
 /*= require bootstrap/popover */
 /*= require select2 */
-/*= require ace/ace */
-/*= require ace/ext-searchbox */
 /*= require underscore */
 /*= require dropzone */
 /*= require mousetrap */
@@ -49,147 +54,89 @@
 /*= require_directory . */
 /*= require fuzzaldrin-plus */
 
-(function() {
-  window.slugify = function(text) {
-    return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase();
-  };
-
-  window.ajaxGet = function(url) {
-    return $.ajax({
-      type: "GET",
-      url: url,
-      dataType: "script"
-    });
-  };
-
-  window.split = function(val) {
-    return val.split(/,\s*/);
-  };
-
-  window.extractLast = function(term) {
-    return split(term).pop();
-  };
-
-  window.rstrip = function(val) {
-    if (val) {
-      return val.replace(/\s+$/, '');
-    } else {
-      return val;
-    }
-  };
+(function () {
+  document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
+  window.addEventListener('hashchange', gl.utils.shiftWindow);
 
-  window.disableButtonIfEmptyField = function(field_selector, button_selector) {
-    var closest_submit, field;
-    field = $(field_selector);
-    closest_submit = field.closest('form').find(button_selector);
-    if (rstrip(field.val()) === "") {
-      closest_submit.disable();
+  window.onload = function () {
+    // Scroll the window to avoid the topnav bar
+    // https://github.com/twitter/bootstrap/issues/1768
+    if (location.hash) {
+      return setTimeout(gl.utils.shiftWindow, 100);
     }
-    return field.on('input', function() {
-      if (rstrip($(this).val()) === "") {
-        return closest_submit.disable();
-      } else {
-        return closest_submit.enable();
-      }
-    });
-  };
-
-  window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
-    var closest_submit, updateButtons;
-    closest_submit = form.find(button_selector);
-    updateButtons = function() {
-      var filled;
-      filled = true;
-      form.find('input').filter(form_selector).each(function() {
-        return filled = rstrip($(this).val()) !== "" || !$(this).attr('required');
-      });
-      if (filled) {
-        return closest_submit.enable();
-      } else {
-        return closest_submit.disable();
-      }
-    };
-    updateButtons();
-    return form.keyup(updateButtons);
-  };
-
-  window.sanitize = function(str) {
-    return str.replace(/<(?:.|\n)*?>/gm, '');
-  };
-
-  window.unbindEvents = function() {
-    return $(document).off('scroll');
   };
 
-  window.shiftWindow = function() {
-    return scrollBy(0, -100);
-  };
-
-  document.addEventListener("page:fetch", unbindEvents);
+  $(function () {
+    var $body = $('body');
+    var $document = $(document);
+    var $window = $(window);
+    var $sidebarGutterToggle = $('.js-sidebar-toggle');
+    var $flash = $('.flash-container');
+    var bootstrapBreakpoint = bp.getBreakpointSize();
+    var checkInitialSidebarSize;
+    var fitSidebarForSize;
 
-  window.addEventListener("hashchange", shiftWindow);
+    // Set the default path for all cookies to GitLab's root directory
+    Cookies.defaults.path = gon.relative_url_root || '/';
 
-  window.onload = function() {
-    if (location.hash) {
-      return setTimeout(shiftWindow, 100);
-    }
-  };
-
-  $(function() {
-    var $body, $document, $sidebarGutterToggle, $window, bootstrapBreakpoint, checkInitialSidebarSize, fitSidebarForSize, flash;
-    $document = $(document);
-    $window = $(window);
-    $body = $('body');
     gl.utils.preventDisabledButtons();
-    bootstrapBreakpoint = bp.getBreakpointSize();
-    $(".nav-sidebar").niceScroll({
+    $('.nav-sidebar').niceScroll({
       cursoropacitymax: '0.4',
       cursorcolor: '#FFF',
-      cursorborder: "1px solid #FFF"
+      cursorborder: '1px solid #FFF'
     });
-    $(".js-select-on-focus").on("focusin", function() {
-      return $(this).select().one('mouseup', function(e) {
+    $('.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() {
-      return $(this).closest('li').fadeOut();
+    $('.remove-row').bind('ajax:success', function () {
+      $(this).tooltip('destroy')
+        .closest('li')
+        .fadeOut();
     });
-    $('.js-remove-tr').bind('ajax:before', function() {
+    $('.js-remove-tr').bind('ajax:before', function () {
       return $(this).hide();
     });
-    $('.js-remove-tr').bind('ajax:success', function() {
+    $('.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() {
+    $('.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) {
-        var $el;
-        $el = $(el);
-        return $el.data('placement') || 'bottom';
+      placement: function (_, el) {
+        return $(el).data('placement') || 'bottom';
       }
     });
-    $('.trigger-submit').on('change', function() {
+    $('.trigger-submit').on('change', function () {
       return $(this).parents('form').submit();
+    // Form submitter
     });
     gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
-    if ((flash = $(".flash-container")).length > 0) {
-      flash.click(function() {
+    // Flash
+    if ($flash.length > 0) {
+      $flash.click(function () {
         return $(this).fadeOut();
       });
-      flash.show();
+      $flash.show();
     }
-    $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
+    // 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) {
@@ -200,58 +147,62 @@
           return buttons.enable();
       }
     });
-    $(document).ajaxError(function(e, xhrObj, xhrSetting, xhrErrorText) {
-      var ref;
+    $(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 = xhrObj.status) === 404 || ref === 500) {
+      } else if (ref === 404 || ref === 500) {
         return new Flash('Something went wrong on our end.', 'alert');
       }
     });
-    $('.account-box').hover(function() {
+    $('.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() {
+    $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() {
+    $('.navbar-toggle').on('click', function () {
       $('.header-content .title').toggle();
       $('.header-content .header-logo').toggle();
       $('.header-content .navbar-collapse').toggle();
       return $('.navbar-toggle').toggleClass('active');
     });
-    $body.on("click", ".js-toggle-diff-comments", function(e) {
-      $(this).toggleClass('active');
-      $(this).closest(".diff-file").find(".notes_holder").toggle();
+    // 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, form, text;
+    $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();
-      btn = $(e.target);
-      text = btn.data("confirm-danger-message");
-      form = btn.closest("form");
       return new ConfirmDangerModal(form, text);
     });
-    $document.on('click', 'button', function() {
-      return $(this).blur();
-    });
-    $('input[type="search"]').each(function() {
-      var $this;
-      $this = $(this);
+    $('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(e) {
+    $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
       var $this;
       $this = $(this);
       return $this.attr('value', $this.val());
     });
-    $sidebarGutterToggle = $('.js-sidebar-toggle');
-    $document.off('breakpoint:change').on('breakpoint:change', function(e, breakpoint) {
+    $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
       var $gutterIcon;
       if (breakpoint === 'sm' || breakpoint === 'xs') {
         $gutterIcon = $sidebarGutterToggle.find('i');
@@ -260,7 +211,7 @@
         }
       }
     });
-    fitSidebarForSize = function() {
+    fitSidebarForSize = function () {
       var oldBootstrapBreakpoint;
       oldBootstrapBreakpoint = bootstrapBreakpoint;
       bootstrapBreakpoint = bp.getBreakpointSize();
@@ -268,56 +219,20 @@
         return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
       }
     };
-    checkInitialSidebarSize = function() {
+    checkInitialSidebarSize = function () {
       bootstrapBreakpoint = bp.getBreakpointSize();
-      if (bootstrapBreakpoint === "xs" || "sm") {
+      if (bootstrapBreakpoint === 'xs' || 'sm') {
         return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
       }
     };
-    $window.off("resize.app").on("resize.app", function(e) {
+    $window.off('resize.app').on('resize.app', function () {
       return fitSidebarForSize();
     });
     gl.awardsHandler = new AwardsHandler();
     checkInitialSidebarSize();
     new Aside();
-    if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
-      $.cookie('pin_nav', 'false', {
-        path: '/',
-        expires: 365 * 10
-      });
-      $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
-      $('.navbar-fixed-top').removeClass('header-pinned-nav');
-    }
-    $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
-      var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText;
-      e.preventDefault();
-      $pinBtn = $(e.currentTarget);
-      $page = $('.page-with-sidebar');
-      $topNav = $('.navbar-fixed-top');
-      $tooltip = $("#" + ($pinBtn.attr('aria-describedby')));
-      doPinNav = !$page.is('.page-sidebar-pinned');
-      tooltipText = 'Pin navigation';
-      $(this).toggleClass('is-active');
-      if (doPinNav) {
-        $page.addClass('page-sidebar-pinned');
-        $topNav.addClass('header-pinned-nav');
-      } else {
-        $tooltip.remove();
-        $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
-        $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
-      }
-      $.cookie('pin_nav', doPinNav, {
-        path: '/',
-        expires: 365 * 10
-      });
-      if ($.cookie('pin_nav') === 'true' || doPinNav) {
-        tooltipText = 'Unpin navigation';
-      }
-      $tooltip.find('.tooltip-inner').text(tooltipText);
-      return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
-    });
 
-    // Custom time ago
-    gl.utils.shortTimeAgo($('.js-short-timeago'));
+    // bind sidebar events
+    new gl.Sidebar();
   });
 }).call(this);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 7b546e79ee06235fa005de4ccd8ffeb6ffb0bbbf..c7eff27f9718edf85e1a50d3164e521907675375 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Aside = (function() {
     function Aside() {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 7116512d6b7be2d5ba3e6062f8d6cb589572f20e..ab09e4475e65100a86044456f642428e62053ef7 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Autosave = (function() {
     function Autosave(field, key) {
@@ -16,7 +17,7 @@
     }
 
     Autosave.prototype.restore = function() {
-      var e, error, text;
+      var e, text;
       if (window.localStorage == null) {
         return;
       }
@@ -41,7 +42,7 @@
       if ((text != null ? text.length : void 0) > 0) {
         try {
           return window.localStorage.setItem(this.key, text);
-        } catch (undefined) {}
+        } catch (error) {}
       } else {
         return this.reset();
       }
@@ -53,7 +54,7 @@
       }
       try {
         return window.localStorage.removeItem(this.key);
-      } catch (undefined) {}
+      } catch (error) {}
     };
 
     return Autosave;
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 2c5b83e4f1e1295a6ec0732c18873b65a690f5c0..d7cda977845b00bc680b48f1ceeca6912ecc678c 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,5 +1,7 @@
+/* eslint-disable */
 (function() {
   this.AwardsHandler = (function() {
+    var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
     function AwardsHandler() {
       this.aliases = gl.emojiAliases();
       $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -85,10 +87,12 @@
     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 != null) && position === 'right') {
+      if (position === 'right') {
         css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
         $menu.addClass('is-aligned-right');
       } else {
@@ -130,7 +134,7 @@
           counter = $emojiButton.find('.js-counter');
           counter.text(parseInt(counter.text()) + 1);
           $emojiButton.addClass('active');
-          this.addMeToUserList(votesBlock, emoji);
+          this.addYouToUserList(votesBlock, emoji);
           return this.animateEmoji($emojiButton);
         }
       } else {
@@ -176,11 +180,11 @@
       counterNumber = parseInt(counter.text(), 10);
       if (counterNumber > 1) {
         counter.text(counterNumber - 1);
-        this.removeMeFromUserList($emojiButton, emoji);
+        this.removeYouFromUserList($emojiButton, emoji);
       } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
         $emojiButton.tooltip('destroy');
         counter.text('0');
-        this.removeMeFromUserList($emojiButton, emoji);
+        this.removeYouFromUserList($emojiButton, emoji);
         if ($emojiButton.parents('.note').length) {
           this.removeEmoji($emojiButton);
         }
@@ -204,43 +208,48 @@
       return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
     };
 
-    AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) {
+    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];
+      }
+    };
+
+    AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
       var authors, awardBlock, newAuthors, originalTitle;
       awardBlock = $emojiButton;
       originalTitle = this.getAwardTooltip(awardBlock);
-      authors = originalTitle.split(', ');
-      authors.splice(authors.indexOf('me'), 1);
-      newAuthors = authors.join(', ');
-      awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors);
-      return this.resetTooltip(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.addMeToUserList = function(votesBlock, emoji) {
+    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(', ');
+        users = origTitle.trim().split(FROM_SENTENCE_REGEX);
       }
-      users.push('me');
-      awardBlock.attr('title', users.join(', '));
-      return this.resetTooltip(awardBlock);
-    };
-
-    AwardsHandler.prototype.resetTooltip = function(award) {
-      var cb;
-      award.tooltip('destroy');
-      cb = function() {
-        return award.tooltip();
-      };
-      return setTimeout(cb, 200);
+      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='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
+      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);
       this.animateEmoji($emojiButton);
@@ -249,12 +258,12 @@
     };
 
     AwardsHandler.prototype.animateEmoji = function($emoji) {
-      var className;
-      className = 'pulse animated';
+      var className = 'pulse animated once short';
       $emoji.addClass(className);
-      return setTimeout((function() {
-        return $emoji.removeClass(className);
-      }), 321);
+
+      $emoji.on('webkitAnimationEnd animationEnd', function() {
+        $(this).removeClass(className);
+      });
     };
 
     AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
@@ -278,6 +287,7 @@
       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;
@@ -313,20 +323,18 @@
       var frequentlyUsedEmojis;
       frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
       frequentlyUsedEmojis.push(emoji);
-      return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
-        expires: 365
-      });
+      Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
     };
 
     AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
       var frequentlyUsedEmojis;
-      frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',');
+      frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
       return _.compact(_.uniq(frequentlyUsedEmojis));
     };
 
     AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
       var emoji, frequentlyUsedEmojis, i, len, ul;
-      if ($.cookie('frequently_used_emojis')) {
+      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++) {
@@ -343,9 +351,11 @@
         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) {
-            h5 = $('<h5>').text('Search results');
+            // 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();
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index f977a1e8a7b072806b0bd7cdaacadfd24ee94262..074378b9e52f6e3dc8a5e0ec3076e5f83fd56b82 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,7 +1,6 @@
+/* eslint-disable */
 
 /*= require jquery.ba-resize */
-
-
 /*= require autosize */
 
 (function() {
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 3631d1b74ac17a6e9da357919cedc1c72f1a8861..a64cefb62bdcbd39384edf51f7ef68ec3a6ce7d0 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   $(function() {
     $("body").on("click", ".js-details-target", function() {
@@ -5,9 +6,20 @@
       container = $(this).closest(".js-details-container");
       return container.toggleClass("open");
     });
+    // Show details content. Hides link after click.
+    //
+    // %div
+    //   %a.js-details-expand
+    //   %div.js-details-content
+    //
     return $("body").on("click", ".js-details-expand", function(e) {
       $(this).next('.js-details-content').removeClass("hide");
       $(this).hide();
+
+      var truncatedItem = $(this).siblings('.js-details-short');
+      if (truncatedItem.length) {
+        truncatedItem.addClass("hide");
+      }
       return e.preventDefault();
     });
   });
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3527d0a95fc584bda085641ce28a822f39640bde..7ff88ecdcaf957873bb84e243b1971720e492450 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,21 @@
-
+/* eslint-disable */
+// Quick Submit behavior
+//
+// When a child field of a form with a `js-quick-submit` class receives a
+// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+// is submitted.
+//
 /*= require extensions/jquery */
 
+//
+// ### Example Markup
+//
+//   <form action="/foo" class="js-quick-submit">
+//     <input type="text" />
+//     <textarea></textarea>
+//     <input type="submit" value="Submit" />
+//   </form>
+//
 (function() {
   var isMac, keyCodeIs;
 
@@ -17,6 +32,7 @@
 
   $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
     var $form, $submit_button;
+    // Enter
     if (!keyCodeIs(e, 13)) {
       return;
     }
@@ -33,8 +49,11 @@
     return $form.submit();
   });
 
+  // If the user tabs to a submit button on a `js-quick-submit` form, display a
+  // tooltip to let them know they could've used the hotkey
   $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
     var $this, title;
+    // Tab
     if (!keyCodeIs(e, 9)) {
       return;
     }
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index db0b36b24e9e53f081ed466fc30e6047541fdd84..4ac343f876c288f05a9e4cd312e6ebfc08d959e9 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,6 +1,19 @@
-
+/* eslint-disable */
+// Requires Input behavior
+//
+// 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 */
 
+//
+// ### Example Markup
+//
+//   <form class="js-requires-input">
+//     <input type="text" required="required">
+//     <input type="submit" value="Submit">
+//   </form>
+//
 (function() {
   $.fn.requiresInput = function() {
     var $button, $form, fieldSelector, requireInput, required;
@@ -11,14 +24,17 @@
     requireInput = function() {
       var values;
       values = _.map($(fieldSelector, $form), function(field) {
+        // Collect the input values of *all* required fields
         return field.value;
       });
+      // Disable the button if any required fields are empty
       if (values.length && _.any(values, _.isEmpty)) {
         return $button.disable();
       } else {
         return $button.enable();
       }
     };
+    // Set initial button state
     requireInput();
     return $form.on('change input', fieldSelector, requireInput);
   };
@@ -27,6 +43,8 @@
     var $form, hideOrShowHelpBlock;
     $form = $('form.js-requires-input');
     $form.requiresInput();
+    // Hide or Show the help block when creating a new project
+    // based on the option selected
     hideOrShowHelpBlock = function(form) {
       var selected;
       selected = $('.js-select-namespace option:selected');
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 1b7b63489ea3fc5eb300a90ed84ada71f3af53fb..05b213fe3fb382000a58f17a5c9a5ba15964df2e 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,10 +1,34 @@
-(function() {
+/* eslint-disable */
+(function(w) {
   $(function() {
-    return $("body").on("click", ".js-toggle-button", function(e) {
-      $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up');
-      $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle();
-      return e.preventDefault();
+    // Toggle button. Show/hide content inside parent container.
+    // Button does not change visibility. If button has icon - it changes chevron style.
+    //
+    // %div.js-toggle-container
+    //   %a.js-toggle-button
+    //   %div.js-toggle-content
+    //
+    $('body').on('click', '.js-toggle-button', function(e) {
+      e.preventDefault();
+      $(this)
+        .find('.fa')
+          .toggleClass('fa-chevron-down fa-chevron-up')
+        .end()
+        .closest('.js-toggle-container')
+          .find('.js-toggle-content')
+            .toggle()
+      ;
     });
-  });
 
-}).call(this);
+    // If we're accessing a permalink, ensure it is not inside a
+    // closed js-toggle-container!
+    var hash = w.gl.utils.getLocationHash();
+    var anchor = hash && document.getElementById(hash);
+    var container = anchor && $(anchor).closest('.js-toggle-container');
+
+    if (container && container.find('.js-toggle-content').is(':hidden')) {
+      container.find('.js-toggle-button').trigger('click');
+      anchor.scrollIntoView();
+    }
+  });
+})(window);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index 6875857496776896297e353b6e6ae38c37e54526..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,46 +0,0 @@
-
-/*= require blob/template_selector */
-
-(function() {
-  var 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.BlobCiYamlSelector = (function(superClass) {
-    extend(BlobCiYamlSelector, superClass);
-
-    function BlobCiYamlSelector() {
-      return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
-    }
-
-    BlobCiYamlSelector.prototype.requestFile = function(query) {
-      return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
-    };
-
-    return BlobCiYamlSelector;
-
-  })(TemplateSelector);
-
-  this.BlobCiYamlSelectors = (function() {
-    function BlobCiYamlSelectors(opts) {
-      var ref;
-      this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
-      this.$dropdowns.each((function(_this) {
-        return function(i, dropdown) {
-          var $dropdown;
-          $dropdown = $(dropdown);
-          return new BlobCiYamlSelector({
-            pattern: /(.gitlab-ci.yml)/,
-            data: $dropdown.data('data'),
-            wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
-            dropdown: $dropdown,
-            editor: _this.editor
-          });
-        };
-      })(this));
-    }
-
-    return BlobCiYamlSelectors;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..37531aaec9b49c89e0b9586e1330660567a829ea
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -0,0 +1,41 @@
+/* eslint-disable */
+/*= require blob/template_selector */
+((global) => {
+
+  class BlobCiYamlSelector extends gl.TemplateSelector {
+    requestFile(query) {
+      return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
+    }
+
+    requestFileSuccess(file) {
+      return super.requestFileSuccess(file);
+    }
+  }
+
+  global.BlobCiYamlSelector = BlobCiYamlSelector;
+
+  class BlobCiYamlSelectors {
+    constructor({ editor, $dropdowns } = {}) {
+      this.editor = editor;
+      this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+      this.initSelectors();
+    }
+
+    initSelectors() {
+      const editor = this.editor;
+      this.$dropdowns.each((i, dropdown) => {
+        const $dropdown = $(dropdown);
+        return new BlobCiYamlSelector({
+          editor,
+          pattern: /(.gitlab-ci.yml)/,
+          data: $dropdown.data('data'),
+          wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+          dropdown: $dropdown
+        });
+      });
+    }
+  }
+
+  global.BlobCiYamlSelectors = BlobCiYamlSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index f4044f22db20f514b2adf7ebbc8dcb50ae08894f..33fb4f8185cb374b82255c2ad282671d0faac2c3 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.BlobFileDropzone = (function() {
     function BlobFileDropzone(form, method) {
@@ -8,6 +9,8 @@
         autoDiscover: false,
         autoProcessQueue: false,
         url: form.attr('action'),
+        // Rails uses a hidden input field for PUT
+        // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
         method: method,
         clickable: true,
         uploadMultiple: false,
@@ -36,6 +39,7 @@
             formData.append('commit_message', form.find('.js-commit-message').val());
           });
         },
+        // Override behavior of adding error underneath preview
         error: function(file, errorMessage) {
           var stripped;
           stripped = $("<div/>").html(errorMessage).text();
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 54a09e919f8c4f74e0b8b17e4f29fe2eb01f953d..344fe5dcd94a62e26cac01f327781c1263f02afa 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require blob/template_selector */
 
@@ -18,6 +19,6 @@
 
     return BlobGitignoreSelector;
 
-  })(TemplateSelector);
+  })(gl.TemplateSelector);
 
 }).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 4e9500428b2e0f7131b5cdc9730fb3eecfcc7f99..9e992f7913c774a728aff98064f94ec71ba1913f 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.BlobGitignoreSelectors = (function() {
     function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 9a8ef08f4e5c7b987f12a93ce8752de00ac4978f..41a83a56146207ae609a3ff93e70b2b2bb468e17 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require blob/template_selector */
 
@@ -23,6 +24,6 @@
 
     return BlobLicenseSelector;
 
-  })(TemplateSelector);
+  })(gl.TemplateSelector);
 
 }).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index 39237705e8d63dfe5b3c545d631273c13511dfa8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,25 +0,0 @@
-(function() {
-  this.BlobLicenseSelectors = (function() {
-    function BlobLicenseSelectors(opts) {
-      var ref;
-      this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
-      this.$dropdowns.each((function(_this) {
-        return function(i, dropdown) {
-          var $dropdown;
-          $dropdown = $(dropdown);
-          return new BlobLicenseSelector({
-            pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
-            data: $dropdown.data('data'),
-            wrapper: $dropdown.closest('.js-license-selector-wrap'),
-            dropdown: $dropdown,
-            editor: _this.editor
-          });
-        };
-      })(this));
-    }
-
-    return BlobLicenseSelectors;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..adeb8ba1318500e3ced0e4f05d620fb7a8f2858b
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -0,0 +1,22 @@
+/* eslint-disable */
+((global) => {
+  class BlobLicenseSelectors {
+    constructor({ $dropdowns, editor }) {
+      this.$dropdowns = $('.js-license-selector');
+      this.editor = editor;
+      this.$dropdowns.each((i, dropdown) => {
+        const $dropdown = $(dropdown);
+        return new BlobLicenseSelector({
+          editor,
+          pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+          data: $dropdown.data('data'),
+          wrapper: $dropdown.closest('.js-license-selector-wrap'),
+          dropdown: $dropdown,
+        });
+      });
+    }
+  }
+
+  global.BlobLicenseSelectors = BlobLicenseSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index b0a37ef0e0a4f57b1b76b51ab24c3f63e48bc432..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,90 +0,0 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
-  this.TemplateSelector = (function() {
-    function TemplateSelector(opts) {
-      var ref;
-      if (opts == null) {
-        opts = {};
-      }
-      this.onClick = bind(this.onClick, this);
-      this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
-      this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
-      this.buildDropdown();
-      this.bindEvents();
-      this.onFilenameUpdate();
-    }
-
-    TemplateSelector.prototype.buildDropdown = function() {
-      return this.dropdown.glDropdown({
-        data: this.data,
-        filterable: true,
-        selectable: true,
-        toggleLabel: this.toggleLabel,
-        search: {
-          fields: ['name']
-        },
-        clicked: this.onClick,
-        text: function(item) {
-          return item.name;
-        }
-      });
-    };
-
-    TemplateSelector.prototype.bindEvents = function() {
-      return this.$input.on('keyup blur', (function(_this) {
-        return function(e) {
-          return _this.onFilenameUpdate();
-        };
-      })(this));
-    };
-
-    TemplateSelector.prototype.toggleLabel = function(item) {
-      return item.name;
-    };
-
-    TemplateSelector.prototype.onFilenameUpdate = function() {
-      var filenameMatches;
-      if (!this.$input.length) {
-        return;
-      }
-      filenameMatches = this.pattern.test(this.$input.val().trim());
-      if (!filenameMatches) {
-        this.wrapper.addClass('hidden');
-        return;
-      }
-      return this.wrapper.removeClass('hidden');
-    };
-
-    TemplateSelector.prototype.onClick = function(item, el, e) {
-      e.preventDefault();
-      return this.requestFile(item);
-    };
-
-    TemplateSelector.prototype.requestFile = function(item) {
-      // This `requestFile` method is an abstract method that should
-      // be added by all subclasses.
-    };
-
-    TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
-      this.editor.setValue(file.content, 1);
-      if (!skipFocus) this.editor.focus();
-    };
-
-    TemplateSelector.prototype.startLoadingSpinner = function() {
-      this.dropdownIcon
-        .addClass('fa-spinner fa-spin')
-        .removeClass('fa-chevron-down');
-    };
-
-    TemplateSelector.prototype.stopLoadingSpinner = function() {
-      this.dropdownIcon
-        .addClass('fa-chevron-down')
-        .removeClass('fa-spinner fa-spin');
-    };
-
-    return TemplateSelector;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..5434a19bcecf3b68237ae77fa9ca115ba03bf674
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -0,0 +1,98 @@
+/* eslint-disable */
+((global) => {
+    class TemplateSelector {
+      constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
+        this.onClick = this.onClick.bind(this);
+        this.dropdown = dropdown;
+        this.data = data;
+        this.pattern = pattern;
+        this.wrapper = wrapper;
+        this.editor = editor;
+        this.fileEndpoint = fileEndpoint;
+        this.$input = $input || $('#file_name');
+        this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+        this.buildDropdown();
+        this.bindEvents();
+        this.onFilenameUpdate();
+
+        this.autosizeUpdateEvent = document.createEvent('Event');
+        this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+      }
+
+      buildDropdown() {
+        return this.dropdown.glDropdown({
+          data: this.data,
+          filterable: true,
+          selectable: true,
+          toggleLabel: this.toggleLabel,
+          search: {
+            fields: ['name']
+          },
+          clicked: this.onClick,
+          text: function(item) {
+            return item.name;
+          }
+        });
+      }
+
+      bindEvents() {
+        return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+      }
+
+      toggleLabel(item) {
+        return item.name;
+      }
+
+      onFilenameUpdate() {
+        var filenameMatches;
+        if (!this.$input.length) {
+          return;
+        }
+        filenameMatches = this.pattern.test(this.$input.val().trim());
+        if (!filenameMatches) {
+          this.wrapper.addClass('hidden');
+          return;
+        }
+        return this.wrapper.removeClass('hidden');
+      }
+
+      onClick(item, el, e) {
+        e.preventDefault();
+        return this.requestFile(item);
+      }
+
+      requestFile(item) {
+        // This `requestFile` method is an abstract method that should
+        // be added by all subclasses.
+      }
+
+      // To be implemented on the extending class
+      // e.g.
+      // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+      requestFileSuccess(file, { skipFocus } = {}) {
+        const oldValue = this.editor.getValue();
+        let newValue = file.content;
+
+        this.editor.setValue(newValue, 1);
+        if (!skipFocus) this.editor.focus();
+
+        if (this.editor instanceof jQuery) {
+          this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+        }
+      }
+
+      startLoadingSpinner() {
+        this.dropdownIcon
+          .addClass('fa-spinner fa-spin')
+          .removeClass('fa-chevron-down');
+      }
+
+      stopLoadingSpinner() {
+        this.dropdownIcon
+          .addClass('fa-chevron-down')
+          .removeClass('fa-spinner fa-spin');
+      }
+    }
+
+    global.TemplateSelector = TemplateSelector;
+  })(window.gl || ( window.gl = {}));
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..b801c10f1688da9bf6659677dab8419e701e1e0b
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,13 @@
+/* eslint-disable */
+/*= require_tree . */
+
+(function() {
+  $(function() {
+    var url = $(".js-edit-blob-form").data("relative-url-root");
+    url += $(".js-edit-blob-form").data("assets-prefix");
+
+    var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
+    new NewCommitForm($('.js-edit-blob-form'));
+  });
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
similarity index 72%
rename from app/assets/javascripts/blob/edit_blob.js
rename to app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8b13a8eeed7a7d055d594ab3f584b8..60840560dd366d944bb66bc7988cb7f9d707a28a 100644
--- a/app/assets/javascripts/blob/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -18,15 +19,18 @@
         return function() {
           return $("#file-content").val(_this.editor.getValue());
         };
+      // Before a form submission, move the content from the Ace editor into the
+      // submitted textarea
       })(this));
       this.initModePanesAndLinks();
-      new BlobLicenseSelectors({
+      this.initSoftWrap();
+      new gl.BlobLicenseSelectors({
         editor: this.editor
       });
       new BlobGitignoreSelectors({
         editor: this.editor
       });
-      new BlobCiYamlSelectors({
+      new gl.BlobCiYamlSelectors({
         editor: this.editor
       });
     }
@@ -48,6 +52,7 @@
       this.$editModePanes.hide();
       currentPane.fadeIn(200);
       if (paneId === "#preview") {
+        this.$toggleButton.hide();
         return $.post(currentLink.data("preview-url"), {
           content: this.editor.getValue()
         }, function(response) {
@@ -55,10 +60,23 @@
           return currentPane.syntaxHighlight();
         });
       } else {
+        this.$toggleButton.show();
         return this.editor.focus();
       }
     };
 
+    EditBlob.prototype.initSoftWrap = function() {
+      this.isSoftWrapped = false;
+      this.$toggleButton = $('.soft-wrap-toggle');
+      this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
+    };
+
+    EditBlob.prototype.toggleSoftWrap = function(e) {
+      this.isSoftWrapped = !this.isSoftWrapped;
+      this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+      this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+    };
+
     return EditBlob;
 
   })();
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..efb22d38513ad21f0dbb5ed677f1a07c887f5fd5
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -0,0 +1,75 @@
+/* eslint-disable */
+//= require vue
+//= require vue-resource
+//= require Sortable
+//= require_tree ./models
+//= require_tree ./stores
+//= require_tree ./services
+//= require_tree ./mixins
+//= require_tree ./filters
+//= require ./components/board
+//= require ./components/board_sidebar
+//= require ./components/new_list_dropdown
+//= require ./vue_resource_interceptor
+
+$(() => {
+  const $boardApp = document.getElementById('board-app'),
+        Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+
+  if (gl.IssueBoardsApp) {
+    gl.IssueBoardsApp.$destroy(true);
+  }
+
+  gl.IssueBoardsApp = new Vue({
+    el: $boardApp,
+    components: {
+      'board': gl.issueBoards.Board,
+      'board-sidebar': gl.issueBoards.BoardSidebar
+    },
+    data: {
+      state: Store.state,
+      loading: true,
+      endpoint: $boardApp.dataset.endpoint,
+      boardId: $boardApp.dataset.boardId,
+      disabled: $boardApp.dataset.disabled === 'true',
+      issueLinkBase: $boardApp.dataset.issueLinkBase,
+      detailIssue: Store.detail
+    },
+    init: Store.create.bind(Store),
+    computed: {
+      detailIssueVisible () {
+        return Object.keys(this.detailIssue.issue).length;
+      }
+    },
+    created () {
+      gl.boardService = new BoardService(this.endpoint, this.boardId);
+    },
+    ready () {
+      Store.disabled = this.disabled;
+      gl.boardService.all()
+        .then((resp) => {
+          resp.json().forEach((board) => {
+            const list = Store.addList(board);
+
+            if (list.type === 'done') {
+              list.position = Infinity;
+            } else if (list.type === 'backlog') {
+              list.position = -1;
+            }
+          });
+
+          Store.addBlankState();
+          this.loading = false;
+        });
+    }
+  });
+
+  gl.IssueBoardsSearch = new Vue({
+    el: '#js-boards-seach',
+    data: {
+      filters: Store.state.filters
+    }
+  });
+});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..0e03d43872b92ed482f975249ef69526340cb367
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -0,0 +1,94 @@
+/* eslint-disable */
+//= require ./board_blank_state
+//= require ./board_delete
+//= require ./board_list
+
+(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.Board = Vue.extend({
+    components: {
+      'board-list': gl.issueBoards.BoardList,
+      'board-delete': gl.issueBoards.BoardDelete,
+      'board-blank-state': gl.issueBoards.BoardBlankState
+    },
+    props: {
+      list: Object,
+      disabled: Boolean,
+      issueLinkBase: String
+    },
+    data () {
+      return {
+        detailIssue: Store.detail,
+        filters: Store.state.filters,
+        showIssueForm: false
+      };
+    },
+    watch: {
+      filters: {
+        handler () {
+          this.list.page = 1;
+          this.list.getIssues(true);
+        },
+        deep: true
+      },
+      detailIssue: {
+        handler () {
+          if (!Object.keys(this.detailIssue.issue).length) return;
+
+          const issue = this.list.findIssue(this.detailIssue.issue.id);
+
+          if (issue) {
+            const boardsList = document.querySelectorAll('.boards-list')[0];
+            const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
+            const left = boardsList.scrollLeft - this.$el.offsetLeft;
+
+            if (right - boardsList.scrollLeft > 0) {
+              boardsList.scrollLeft = right;
+            } else if (left > 0) {
+              boardsList.scrollLeft = this.$el.offsetLeft;
+            }
+          }
+        },
+        deep: true
+      }
+    },
+    methods: {
+      showNewIssueForm() {
+        this.showIssueForm = !this.showIssueForm;
+      }
+    },
+    ready () {
+      const options = gl.issueBoards.getBoardSortableDefaultOptions({
+        disabled: this.disabled,
+        group: 'boards',
+        draggable: '.is-draggable',
+        handle: '.js-board-handle',
+        onEnd: (e) => {
+          gl.issueBoards.onEnd();
+
+          if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+            const order = this.sortable.toArray(),
+                  $board = this.$parent.$refs.board[e.oldIndex + 1],
+                  list = $board.list;
+
+            $board.$destroy(true);
+
+            this.$nextTick(() => {
+              Store.state.lists.splice(e.newIndex, 0, list);
+              Store.moveList(list, order);
+            });
+          }
+        }
+      });
+
+      this.sortable = Sortable.create(this.$el.parentNode, options);
+    },
+    beforeDestroy () {
+      Store.state.lists.$remove(this.list);
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..885553690d30ea89e71a13cdee5d1e71b680782a
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -0,0 +1,48 @@
+/* eslint-disable */
+(() => {
+  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
+            }
+          });
+        });
+
+        // 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.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..2f6c03e35386b1f8d8f078a7c4bff2d3b336c3a6
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -0,0 +1,78 @@
+/* eslint-disable */
+(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardCard = Vue.extend({
+    props: {
+      list: Object,
+      issue: Object,
+      issueLinkBase: String,
+      disabled: Boolean,
+      index: Number
+    },
+    data () {
+      return {
+        showDetail: false,
+        detailIssue: Store.detail
+      };
+    },
+    computed: {
+      issueDetailVisible () {
+        return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+      }
+    },
+    methods: {
+      filterByLabel (label, e) {
+        let labelToggleText = label.title;
+        const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
+        $(e.target).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}" />`);
+        } 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();
+        }
+
+        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);
+
+        Store.updateFiltersUrl();
+      },
+      mouseDown () {
+        this.showDetail = true;
+      },
+      mouseMove () {
+        if (this.showDetail) {
+          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;
+          }
+        }
+      }
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..c45e1926c5ca6f87bc7afff6175b6323824365ce
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -0,0 +1,20 @@
+/* eslint-disable */
+(() => {
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardDelete = Vue.extend({
+    props: {
+      list: Object
+    },
+    methods: {
+      deleteBoard () {
+        $(this.$el).tooltip('hide');
+
+        if (confirm('Are you sure you want to delete this list?')) {
+          this.list.destroy();
+        }
+      }
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..34fc76942410f42ea246c72985d7f470067ce154
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -0,0 +1,107 @@
+/* eslint-disable */
+//= require ./board_card
+//= require ./board_new_issue
+
+(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardList = Vue.extend({
+    components: {
+      'board-card': gl.issueBoards.BoardCard,
+      'board-new-issue': gl.issueBoards.BoardNewIssue
+    },
+    props: {
+      disabled: Boolean,
+      list: Object,
+      issues: Array,
+      loading: Boolean,
+      issueLinkBase: String,
+      showIssueForm: Boolean
+    },
+    data () {
+      return {
+        scrollOffset: 250,
+        filters: Store.state.filters,
+        showCount: false
+      };
+    },
+    watch: {
+      filters: {
+        handler () {
+          this.list.loadingMore = false;
+          this.$els.list.scrollTop = 0;
+        },
+        deep: true
+      },
+      issues () {
+        this.$nextTick(() => {
+          if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
+            this.list.page++;
+            this.list.getIssues(false);
+          }
+
+          if (this.scrollHeight() > this.listHeight()) {
+            this.showCount = true;
+          } else {
+            this.showCount = false;
+          }
+        });
+      }
+    },
+    methods: {
+      listHeight () {
+        return this.$els.list.getBoundingClientRect().height;
+      },
+      scrollHeight () {
+        return this.$els.list.scrollHeight;
+      },
+      scrollTop () {
+        return this.$els.list.scrollTop + this.listHeight();
+      },
+      loadNextPage () {
+        const getIssues = this.list.nextPage();
+
+        if (getIssues) {
+          this.list.loadingMore = true;
+          getIssues.then(() => {
+            this.list.loadingMore = false;
+          });
+        }
+      },
+    },
+    ready () {
+      const options = gl.issueBoards.getBoardSortableDefaultOptions({
+        group: 'issues',
+        sort: false,
+        disabled: this.disabled,
+        filter: '.board-list-count, .is-disabled',
+        onStart: (e) => {
+          const card = this.$refs.issue[e.oldIndex];
+
+          Store.moving.issue = card.issue;
+          Store.moving.list = card.list;
+
+          gl.issueBoards.onStart();
+        },
+        onAdd: (e) => {
+          gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+        },
+        onRemove: (e) => {
+          this.$refs.issue[e.oldIndex].$destroy(true);
+        }
+      });
+
+      this.sortable = Sortable.create(this.$els.list, options);
+
+      // Scroll event on list to load more
+      this.$els.list.onscroll = () => {
+        if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+          this.loadNextPage();
+        }
+      };
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..7fc0bfd56f3da72df78a1106a7721d311df09bac
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -0,0 +1,64 @@
+/* eslint-disable */
+(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+
+  gl.issueBoards.BoardNewIssue = Vue.extend({
+    props: {
+      list: Object,
+      showIssueForm: Boolean
+    },
+    data() {
+      return {
+        title: '',
+        error: false
+      };
+    },
+    watch: {
+      showIssueForm () {
+        this.$els.input.focus();
+      }
+    },
+    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.$els.submitButton).enable();
+
+            Store.detail.issue = issue;
+          })
+          .catch(() => {
+            // Need this because our jQuery very kindly disables buttons on ALL form submissions
+            $(this.$els.submitButton).enable();
+
+            // Remove the issue
+            this.list.removeIssue(issue);
+
+            // Show error message
+            this.error = true;
+            this.showIssueForm = true;
+          });
+
+        this.cancel();
+      },
+      cancel() {
+        this.showIssueForm = false;
+        this.title = '';
+      }
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..4928320d0155c2ae188e878f4cdd7527bb04f44e
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -0,0 +1,53 @@
+/* eslint-disable */
+(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardSidebar = Vue.extend({
+    props: {
+      currentUser: Object
+    },
+    data() {
+      return {
+        detail: Store.detail,
+        issue: {}
+      };
+    },
+    computed: {
+      showSidebar () {
+        return Object.keys(this.issue).length;
+      }
+    },
+    watch: {
+      detail: {
+        handler () {
+          this.issue = this.detail.issue;
+        },
+        deep: true
+      },
+      issue () {
+        if (this.showSidebar) {
+          this.$nextTick(() => {
+            $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+            $('.right-sidebar').getNiceScroll().resize();
+          });
+        }
+      }
+    },
+    methods: {
+      closeSidebar () {
+        this.detail.issue = {};
+      }
+    },
+    ready () {
+      new IssuableContext(this.currentUser);
+      new MilestoneSelect();
+      new gl.DueDateSelectors();
+      new LabelsSelect();
+      new Sidebar();
+      new Subscription('.subscription');
+    }
+  });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..14f618fd5d557579695c64be7d4b0ec02fda181f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -0,0 +1,68 @@
+/* eslint-disable */
+$(() => {
+  const Store = gl.issueBoards.BoardsStore;
+
+  $(document).off('created.label').on('created.label', (e, label) => {
+    Store.new({
+      title: label.title,
+      position: Store.state.lists.length - 2,
+      list_type: 'label',
+      label: {
+        id: label.id,
+        title: label.title,
+        color: label.color
+      }
+    });
+  });
+
+  $('.js-new-board-list').each(function () {
+    const $this = $(this);
+    new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+
+    $this.glDropdown({
+      data(term, callback) {
+        $.get($this.attr('data-labels'))
+          .then((resp) => {
+            callback(resp);
+          });
+      },
+      renderRow (label) {
+        const active = Store.findList('title', label.title),
+              $li = $('<li />'),
+              $a = $('<a />', {
+                class: (active ? `is-active js-board-list-${active.id}` : ''),
+                text: label.title,
+                href: '#'
+              }),
+              $labelColor = $('<span />', {
+                class: 'dropdown-label-box',
+                style: `background-color: ${label.color}`
+              });
+
+        return $li.append($a.prepend($labelColor));
+      },
+			search: {
+				fields: ['title']
+			},
+			filterable: true,
+      selectable: true,
+      multiSelect: true,
+      clicked (label, $el, e) {
+        e.preventDefault();
+
+        if (!Store.findList('title', label.title)) {
+          Store.new({
+            title: label.title,
+            position: Store.state.lists.length - 2,
+            list_type: 'label',
+            label: {
+              id: label.id,
+              title: label.title,
+              color: label.color
+            }
+          });
+        }
+      }
+    });
+  });
+});
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9eceac4edddebefa9b8c30e02f57e74e9c8c0cd9
--- /dev/null
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -0,0 +1,5 @@
+/* eslint-disable */
+Vue.filter('due-date', (value) => {
+  const date = new Date(value);
+  return $.datepicker.formatDate('M d, yy', date);
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..db9a5a8e40a3e7728e1323b030092223b36e750d
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -0,0 +1,36 @@
+/* eslint-disable */
+((w) => {
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.onStart = () => {
+    $('.has-tooltip').tooltip('hide')
+      .tooltip('disable');
+    document.body.classList.add('is-dragging');
+  };
+
+  gl.issueBoards.onEnd = () => {
+    $('.has-tooltip').tooltip('enable');
+    document.body.classList.remove('is-dragging');
+  };
+
+  gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+
+  gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+    let defaultSortOptions = {
+      forceFallback: true,
+      fallbackClass: 'is-dragging',
+      fallbackOnBody: true,
+      ghostClass: 'is-ghost',
+      filter: '.board-delete, .btn',
+      delay: gl.issueBoards.touchEnabled ? 100 : 50,
+      scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
+      scrollSpeed: 20,
+      onStart: gl.issueBoards.onStart,
+      onEnd: gl.issueBoards.onEnd
+    }
+
+    Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+    return defaultSortOptions;
+  };
+})(window);
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..21d735e8231e6e5ec0c81b9d250f1b5bc68f13b4
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -0,0 +1,68 @@
+/* eslint-disable */
+class ListIssue {
+  constructor (obj) {
+    this.id = obj.iid;
+    this.title = obj.title;
+    this.confidential = obj.confidential;
+    this.dueDate = obj.due_date;
+    this.subscribed = obj.subscribed;
+    this.labels = [];
+
+    if (obj.assignee) {
+      this.assignee = new ListUser(obj.assignee);
+    }
+
+    if (obj.milestone) {
+      this.milestone = new ListMilestone(obj.milestone);
+    }
+
+    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) {
+    if (!this.findLabel(label)) {
+      this.labels.push(new ListLabel(label));
+    }
+  }
+
+  findLabel (findLabel) {
+    return this.labels.filter( label => label.title === findLabel.title )[0];
+  }
+
+  removeLabel (removeLabel) {
+    if (removeLabel) {
+      this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+    }
+  }
+
+  removeLabels (labels) {
+    labels.forEach(this.removeLabel.bind(this));
+  }
+
+  getLists () {
+    return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+  }
+
+  update (url) {
+    const data = {
+      issue: {
+        milestone_id: this.milestone ? this.milestone.id : null,
+        due_date: this.dueDate,
+        assignee_id: this.assignee ? this.assignee.id : null,
+        label_ids: this.labels.map( (label) => label.id )
+      }
+    };
+
+    if (!data.issue.label_ids.length) {
+      data.issue.label_ids = [''];
+    }
+
+    return Vue.http.patch(url, data);
+  }
+}
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..0910fe9a8540800646b377c372bc5dc5cb5e16aa
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -0,0 +1,11 @@
+/* eslint-disable */
+class ListLabel {
+  constructor (obj) {
+    this.id = obj.id;
+    this.title = obj.title;
+    this.color = obj.color;
+    this.textColor = obj.text_color;
+    this.description = obj.description;
+    this.priority = (obj.priority !== null) ? obj.priority : Infinity;
+  }
+}
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..b331a26fed59f1500ff9677202a0a60c1165145e
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -0,0 +1,142 @@
+/* eslint-disable */
+class List {
+  constructor (obj) {
+    this.id = obj.id;
+    this._uid = this.guid();
+    this.position = obj.position;
+    this.title = obj.title;
+    this.type = obj.list_type;
+    this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+    this.filters = gl.issueBoards.BoardsStore.state.filters;
+    this.page = 1;
+    this.loading = true;
+    this.loadingMore = false;
+    this.issues = [];
+    this.issuesSize = 0;
+
+    if (obj.label) {
+      this.label = new ListLabel(obj.label);
+    }
+
+    if (this.type !== 'blank' && this.id) {
+      this.getIssues();
+    }
+  }
+
+  guid() {
+    const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+    return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
+  }
+
+  save () {
+    return gl.boardService.createList(this.label.id)
+      .then((resp) => {
+        const data = resp.json();
+
+        this.id = data.id;
+        this.type = data.list_type;
+        this.position = data.position;
+
+        return this.getIssues();
+      });
+  }
+
+  destroy () {
+    gl.issueBoards.BoardsStore.state.lists.$remove(this);
+    gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
+
+    gl.boardService.destroyList(this.id);
+  }
+
+  update () {
+    gl.boardService.updateList(this.id, this.position);
+  }
+
+  nextPage () {
+    if (this.issuesSize > this.issues.length) {
+      this.page++;
+
+      return this.getIssues(false);
+    }
+  }
+
+  getIssues (emptyIssues = true) {
+    const filters = this.filters;
+    let data = { page: this.page };
+
+    Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
+
+    if (this.label) {
+      data.label_name = data.label_name.filter( label => label !== this.label.title );
+    }
+
+    if (emptyIssues) {
+      this.loading = true;
+    }
+
+    return gl.boardService.getIssuesForList(this.id, data)
+      .then((resp) => {
+        const data = resp.json();
+        this.loading = false;
+        this.issuesSize = data.size;
+
+        if (emptyIssues) {
+          this.issues = [];
+        }
+
+        this.createIssues(data.issues);
+      });
+  }
+
+  newIssue (issue) {
+    this.addIssue(issue);
+    this.issuesSize++;
+
+    return gl.boardService.newIssue(this.id, issue)
+      .then((resp) => {
+        const data = resp.json();
+        issue.id = data.iid;
+      });
+  }
+
+  createIssues (data) {
+    data.forEach((issueObj) => {
+      this.addIssue(new ListIssue(issueObj));
+    });
+  }
+
+  addIssue (issue, listFrom) {
+    if (!this.findIssue(issue.id)) {
+      this.issues.push(issue);
+
+      if (this.label) {
+        issue.addLabel(this.label);
+      }
+
+      if (listFrom) {
+        this.issuesSize++;
+        gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
+          .then(() => {
+            listFrom.getIssues(false);
+          });
+      }
+    }
+  }
+
+  findIssue (id) {
+    return this.issues.filter( issue => issue.id === id )[0];
+  }
+
+  removeIssue (removeIssue) {
+    this.issues = this.issues.filter((issue) => {
+      const matchesRemove = removeIssue.id === issue.id;
+
+      if (matchesRemove) {
+        this.issuesSize--;
+        issue.removeLabel(this.label);
+      }
+
+      return !matchesRemove;
+    });
+  }
+}
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..a48969e19c93c5140c28ef90d4e119cfb17b90a8
--- /dev/null
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable */
+class ListMilestone {
+  constructor (obj) {
+    this.id = obj.id;
+    this.title = obj.title;
+  }
+}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..583a973fc4676fe4dc523eed9aaff0cac0ecf175
--- /dev/null
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -0,0 +1,9 @@
+/* eslint-disable */
+class ListUser {
+  constructor (user) {
+    this.id = user.id;
+    this.name = user.name;
+    this.username = user.username;
+    this.avatar = user.avatar_url;
+  }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..f59a2ed7937a2eda7f378fbfd3689e965c1e98d1
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -0,0 +1,66 @@
+/* eslint-disable */
+class BoardService {
+  constructor (root, boardId) {
+    this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
+      generate: {
+        method: 'POST',
+        url: `${root}/${boardId}/lists/generate.json`
+      }
+    });
+    this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
+    this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
+
+    Vue.http.interceptors.push((request, next) => {
+      request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+      next();
+    });
+  }
+
+  all () {
+    return this.lists.get();
+  }
+
+  generateDefaultLists () {
+    return this.lists.generate({});
+  }
+
+  createList (label_id) {
+    return this.lists.save({}, {
+      list: {
+        label_id
+      }
+    });
+  }
+
+  updateList (id, position) {
+    return this.lists.update({ id }, {
+      list: {
+        position
+      }
+    });
+  }
+
+  destroyList (id) {
+    return this.lists.delete({ id });
+  }
+
+  getIssuesForList (id, filter = {}) {
+    let data = { id };
+    Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+
+    return this.issues.get(data);
+  }
+
+  moveIssue (id, from_list_id, to_list_id) {
+    return this.issue.update({ id }, {
+      from_list_id,
+      to_list_id
+    });
+  }
+
+  newIssue (id, issue) {
+    return this.issues.save({ id }, {
+      issue
+    });
+  }
+};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..175e034afede2508ef0a4d083e2c6336a7fa2279
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -0,0 +1,118 @@
+/* eslint-disable */
+(() => {
+  window.gl = window.gl || {};
+  window.gl.issueBoards = window.gl.issueBoards || {};
+
+  gl.issueBoards.BoardsStore = {
+    disabled: false,
+    state: {},
+    detail: {
+      issue: {}
+    },
+    moving: {
+      issue: {},
+      list: {}
+    },
+    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: ''
+      };
+    },
+    addList (listObj) {
+      const list = new List(listObj);
+      this.state.lists.push(list);
+
+      return list;
+    },
+    new (listObj) {
+      const list = this.addList(listObj),
+            backlogList = this.findList('type', 'backlog', 'backlog');
+
+      list
+        .save()
+        .then(() => {
+          // Remove any new issues from the backlog
+          // as they will be visible in the new list
+          list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+        });
+      this.removeBlankState();
+    },
+    updateNewListDropdown (listId) {
+      $(`.js-board-list-${listId}`).removeClass('is-active');
+    },
+    shouldAddBlankState () {
+      // Decide whether to add the blank state
+      return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+    },
+    addBlankState () {
+      if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
+
+      this.addList({
+        id: 'blank',
+        list_type: 'blank',
+        title: 'Welcome to your Issue Board!',
+        position: 0
+      });
+    },
+    removeBlankState () {
+      this.removeList('blank');
+
+      Cookies.set('issue_board_welcome_hidden', 'true', {
+        expires: 365 * 10,
+        path: ''
+      });
+    },
+    welcomeIsHidden () {
+      return Cookies.get('issue_board_welcome_hidden') === 'true';
+    },
+    removeList (id, type = 'blank') {
+      const list = this.findList('id', id, type);
+
+      if (!list) return;
+
+      this.state.lists = this.state.lists.filter( list => list.id !== id );
+    },
+    moveList (listFrom, orderLists) {
+      orderLists.forEach((id, i) => {
+        const list = this.findList('id', parseInt(id));
+
+        list.position = i;
+      });
+      listFrom.update();
+    },
+    moveIssueToList (listFrom, listTo, issue) {
+      const issueTo = listTo.findIssue(issue.id),
+            issueLists = issue.getLists(),
+            listLabels = issueLists.map( listIssue => listIssue.label );
+
+      // Add to new lists issues if it doesn't already exist
+      if (!issueTo) {
+        listTo.addIssue(issue, listFrom);
+      }
+
+      if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+        issueLists.forEach((list) => {
+          list.removeIssue(issue);
+        })
+        issue.removeLabels(listLabels);
+      } else {
+        listFrom.removeIssue(issue);
+      }
+    },
+    findList (key, val, type = 'label') {
+      return this.state.lists.filter((list) => {
+        const byType = type ? list['type'] === type : true;
+
+        return list[key] === val && byType;
+      })[0];
+    },
+    updateFiltersUrl () {
+      history.pushState(null, null, `?${$.param(this.state.filters)}`);
+    }
+  };
+})();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
new file mode 100644
index 0000000000000000000000000000000000000000..039ca491cf5e48f476c3b3935f9b87197da42b96
--- /dev/null
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -0,0 +1,120 @@
+/* eslint-disable */
+(function () {
+	'use strict';
+
+	function simulateEvent(el, type, options) {
+		var event;
+		if (!el) return;
+		var ownerDocument = el.ownerDocument;
+
+		options = options || {};
+
+		if (/^mouse/.test(type)) {
+			event = ownerDocument.createEvent('MouseEvents');
+			event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+				options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+				options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+		} else {
+			event = ownerDocument.createEvent('CustomEvent');
+
+			event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+				options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+				options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+			event.dataTransfer = {
+				data: {},
+
+				setData: function (type, val) {
+					this.data[type] = val;
+				},
+
+				getData: function (type) {
+					return this.data[type];
+				}
+			};
+		}
+
+		if (el.dispatchEvent) {
+			el.dispatchEvent(event);
+		} else if (el.fireEvent) {
+			el.fireEvent('on' + type, event);
+		}
+
+		return event;
+	}
+
+	function getTraget(target) {
+		var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+		var children = el.children;
+
+		return (
+			children[target.index] ||
+			children[target.index === 'first' ? 0 : -1] ||
+			children[target.index === 'last' ? children.length - 1 : -1]
+		);
+	}
+
+	function getRect(el) {
+		var rect = el.getBoundingClientRect();
+		var width = rect.right - rect.left;
+		var height = rect.bottom - rect.top;
+
+		return {
+			x: rect.left,
+			y: rect.top,
+			cx: rect.left + width / 2,
+			cy: rect.top + height / 2,
+			w: width,
+			h: height,
+			hw: width / 2,
+			wh: height / 2
+		};
+	}
+
+	function simulateDrag(options, callback) {
+		options.to.el = options.to.el || options.from.el;
+
+		var fromEl = getTraget(options.from);
+		var toEl = getTraget(options.to);
+    var scrollable = options.scrollable;
+
+		var fromRect = getRect(fromEl);
+		var toRect = getRect(toEl);
+
+		var startTime = new Date().getTime();
+		var duration = options.duration || 1000;
+		simulateEvent(fromEl, 'mousedown', {button: 0});
+		options.ontap && options.ontap();
+		window.SIMULATE_DRAG_ACTIVE = 1;
+
+		var dragInterval = setInterval(function loop() {
+			var progress = (new Date().getTime() - startTime) / duration;
+			var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+			var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+			var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+			simulateEvent(overEl, 'mousemove', {
+				clientX: x,
+				clientY: y
+			});
+
+			if (progress >= 1) {
+				options.ondragend && options.ondragend();
+				simulateEvent(toEl, 'mouseup');
+				clearInterval(dragInterval);
+				window.SIMULATE_DRAG_ACTIVE = 0;
+			}
+		}, 100);
+
+		return {
+			target: fromEl,
+			fromList: fromEl.parentNode,
+			toList: toEl.parentNode
+		};
+	}
+
+
+	// Export
+	window.simulateEvent = simulateEvent;
+	window.simulateDrag = simulateDrag;
+})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..80f137ca12ea87a7f9704e67db61f113faf14534
--- /dev/null
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -0,0 +1,8 @@
+/* eslint-disable */
+Vue.http.interceptors.push((request, next) => {
+  Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+  next(function (response) {
+    Vue.activeResources--;
+  });
+});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 1e0148e579817a38b91e81c3428d39d3bdae303b..5d4d23e26c6a4608d35ed551117cf1c90436195a 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Breakpoints = (function() {
     var BreakpointInstance, instance;
@@ -23,6 +24,7 @@
         if ($(allDeviceSelector.join(",")).length) {
           return;
         }
+        // Create all the elements
         els = $.map(BREAKPOINTS, function(breakpoint) {
           return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
         });
@@ -40,6 +42,7 @@
       BreakpointInstance.prototype.getBreakpointSize = function() {
         var $visibleDevice;
         $visibleDevice = this.visibleDevice;
+        // the page refreshed via turbolinks
         if (!$visibleDevice().length) {
           this.setup();
         }
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index fceeff3672851ec4e5d7fbda347ade7bfec3a8d3..576f4c76c1eacefafe7a529f9be973afda85a566 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   $(function() {
     var previewPath;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 3d9b824d40618af5a509fe56f1653576e5029bcd..5133e3610010bf637635fa95cd9060d278997088 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -6,39 +7,53 @@
 
     Build.state = null;
 
-    function Build(page_url, build_url, build_status, state1) {
-      this.page_url = page_url;
-      this.build_url = build_url;
-      this.build_status = build_status;
-      this.state = state1;
-      this.hideSidebar = bind(this.hideSidebar, this);
-      this.toggleSidebar = bind(this.toggleSidebar, this);
+    function Build(options) {
+      options = options || $('.js-build-options').data();
+      this.pageUrl = options.pageUrl;
+      this.buildUrl = options.buildUrl;
+      this.buildStatus = options.buildStatus;
+      this.state = options.state1;
+      this.buildStage = options.buildStage;
+      this.updateDropdown = bind(this.updateDropdown, this);
+      this.$document = $(document);
       clearInterval(Build.interval);
+      // Init breakpoint checker
       this.bp = Breakpoints.get();
-      this.hideSidebar();
-      $('.js-build-sidebar').niceScroll();
-      $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
-      $(window).off('resize.build').on('resize.build', this.hideSidebar);
+
+      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);
+      $(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.build_status === "running" || this.build_status === "pending") {
+      if (this.buildStatus === "running" || this.buildStatus === "pending") {
+        // Bind autoscroll button to follow build output
         $('#autoscroll-button').on('click', function() {
           var state;
           state = $(this).data("state");
           if ("enabled" === state) {
             $(this).data("state", "disabled");
-            return $(this).text("enable autoscroll");
+            return $(this).text("Enable autoscroll");
           } else {
             $(this).data("state", "enabled");
-            return $(this).text("disable autoscroll");
+            return $(this).text("Disable autoscroll");
           }
         });
         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 (window.location.href.split("#").first() === _this.page_url) {
+            if (_this.location() === _this.pageUrl) {
               return _this.getBuildTrace();
             }
           };
@@ -46,13 +61,33 @@
       }
     }
 
+    Build.prototype.initSidebar = function() {
+      this.$sidebar = $('.js-build-sidebar');
+      this.sidebarTranslationLimits = {
+        min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
+      }
+      this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
+      this.$sidebar.css({
+        top: this.sidebarTranslationLimits.max
+      });
+      this.$sidebar.niceScroll();
+      this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+      this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
+    };
+
+    Build.prototype.location = function() {
+      return window.location.href.split("#")[0];
+    };
+
     Build.prototype.getInitialBuildTrace = function() {
+      var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+
       return $.ajax({
-        url: this.build_url,
+        url: this.buildUrl,
         dataType: 'json',
-        success: function(build_data) {
-          $('.js-build-output').html(build_data.trace_html);
-          if (build_data.status === 'success' || build_data.status === 'failed') {
+        success: function(buildData) {
+          $('.js-build-output').html(buildData.trace_html);
+          if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
             return $('.js-build-refresh').remove();
           }
         }
@@ -61,7 +96,7 @@
 
     Build.prototype.getBuildTrace = function() {
       return $.ajax({
-        url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
+        url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
         dataType: "json",
         success: (function(_this) {
           return function(log) {
@@ -75,8 +110,8 @@
                 $('.js-build-output').html(log.html);
               }
               return _this.checkAutoscroll();
-            } else if (log.status !== _this.build_status) {
-              return Turbolinks.visit(_this.page_url);
+            } else if (log.status !== _this.buildStatus) {
+              return Turbolinks.visit(_this.pageUrl);
             }
           };
         })(this)
@@ -90,11 +125,10 @@
     };
 
     Build.prototype.initScrollButtonAffix = function() {
-      var $body, $buildScroll, $buildTrace;
-      $buildScroll = $('#js-build-scroll');
+      var $body, $buildTrace;
       $body = $('body');
       $buildTrace = $('#build-trace');
-      return $buildScroll.affix({
+      return this.$buildScroll.affix({
         offset: {
           bottom: function() {
             return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
@@ -103,24 +137,34 @@
       });
     };
 
-    Build.prototype.shouldHideSidebar = function() {
+    Build.prototype.shouldHideSidebarForViewport = function() {
       var bootstrapBreakpoint;
       bootstrapBreakpoint = this.bp.getBreakpointSize();
       return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
     };
 
-    Build.prototype.toggleSidebar = function() {
-      if (this.shouldHideSidebar()) {
-        return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed');
-      }
+    Build.prototype.translateSidebar = function(e) {
+      var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
+      if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
+      this.$sidebar.css({
+        top: newPosition
+      });
     };
 
-    Build.prototype.hideSidebar = function() {
-      if (this.shouldHideSidebar()) {
-        return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
-      } else {
-        return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
-      }
+    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() {
@@ -128,10 +172,34 @@
       $date = $('.js-artifacts-remove');
       if ($date.length) {
         date = $date.text();
-        return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
+        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) {
+      e.preventDefault();
+      $currentTarget = $(e.currentTarget);
+      $.scrollTo($currentTarget.attr('href'), {
+        offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+      });
+    };
+
     return Build;
 
   })();
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index f345ba0abe624892d8ffd0b700d20a531e0a1e83..49f84581650720e8f435292d583c015df8f483a6 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.BuildArtifacts = (function() {
     function BuildArtifacts() {
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..0ecd20bc11e10331a2755aae3e972a09f4c455de
--- /dev/null
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable */
+$(function(){
+  $('.reveal-variables').off('click').on('click',function(){
+    $('.js-build').toggle().niceScroll();
+    $(this).hide();
+  });
+});
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 23cf5b519f457f438ef29218223378eef66ce649..fac5b4f17da551a05af3d5188c9f3ad5fe784b66 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Commit = (function() {
     function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index be24ee56aad57e37b6af5373deb1a9a8e2013973..16d63729d3184a343a9d86a0c60e083adb9368c6 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.CommitFile = (function() {
     function CommitFile(file) {
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js
similarity index 97%
rename from app/assets/javascripts/commit/image-file.js
rename to app/assets/javascripts/commit/image_file.js
index c0d0b2d049fae7feb9eeb7ce431df914a518a6ac..ffddce1297bba87c842d2524c67fbaaac450d304 100644
--- a/app/assets/javascripts/commit/image-file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,7 +1,9 @@
+/* eslint-disable */
 (function() {
   this.ImageFile = (function() {
     var prepareFrames;
 
+    // Width where images must fits in, for 2-up this gets divided by 2
     ImageFile.availWidth = 900;
 
     ImageFile.viewModes = ['two-up', 'swipe'];
@@ -9,6 +11,7 @@
     function ImageFile(file) {
       this.file = file;
       this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
+        // Determine if old and new file has same dimensions, if not show 'two-up' view
         return function(deletedWidth, deletedHeight) {
           return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
             if (width === deletedWidth && height === deletedHeight) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 37f168c51902ba2b5b01efb0614815e5fdd3a166..c765d233831f2eb47265826caf03f48bea98697f 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.CommitsList = (function() {
     function CommitsList() {}
@@ -45,6 +46,7 @@
           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);
         },
         dataType: "json"
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 342ac0e8e69d0279811f302aba81f6ef184db15f..61cc91c524bafe812d51a9234597488bb293a881 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Compare = (function() {
     function Compare(opts) {
@@ -79,7 +80,8 @@
         success: function(html) {
           loading.hide();
           $target.html(html);
-          return $('.js-timeago', $target).timeago();
+          var className = '.' + $target[0].className.replace(' ', '.');
+          gl.utils.localTimeAgo($('.js-timeago', className));
         }
       });
     };
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js.es6
similarity index 65%
rename from app/assets/javascripts/compare_autocomplete.js
rename to app/assets/javascripts/compare_autocomplete.js.es6
index 4e3a28cd163ba9b299e7e984066eccf4335d05f4..bd980f87e7222e78f9e5e761ed250c56a7b3dd5d 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.CompareAutocomplete = (function() {
     function CompareAutocomplete() {
@@ -9,7 +10,10 @@
         var $dropdown, selected;
         $dropdown = $(this);
         selected = $dropdown.data('selected');
-        return $dropdown.glDropdown({
+        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'),
@@ -23,8 +27,9 @@
           selectable: true,
           filterable: true,
           filterByText: true,
-          fieldName: $dropdown.attr('name'),
-          filterInput: 'input[type="text"]',
+          toggleLabel: true,
+          fieldName: $dropdown.data('field-name'),
+          filterInput: 'input[type="search"]',
           renderRow: function(ref) {
             var link;
             if (ref.header != null) {
@@ -41,6 +46,14 @@
             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');
+        });
       });
     };
 
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 708ab08ffacbd083449289d684fa3f7b573bd657..143d21adb37c641aba8fde2fce467ebd94c8f5cd 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ConfirmDangerModal = (function() {
     function ConfirmDangerModal(form, text) {
@@ -11,7 +12,7 @@
       submit.disable();
       $('.js-confirm-danger-input').off('input');
       $('.js-confirm-danger-input').on('input', function() {
-        if (rstrip($(this).val()) === project_path) {
+        if (gl.utils.rstrip($(this).val()) === project_path) {
           return submit.enable();
         } else {
           return submit.disable();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c82798cc6a58b079e268969b646db5dffedba3b2..7808d7fe313eb1eb6dd05a42d8ad103cccd170d0 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require clipboard */
 
@@ -6,14 +7,19 @@
 
   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;';
+      key = '&#8984;'; // Command
     } else {
       key = 'Ctrl';
     }
@@ -21,19 +27,20 @@
   };
 
   showTooltip = function(target, title) {
-    return $(target).tooltip({
-      container: 'body',
-      html: 'true',
-      placement: 'auto bottom',
-      title: title,
-      trigger: 'manual'
-    }).tooltip('show').one('mouseleave', function() {
-      return $(this).tooltip('hide');
-    });
+    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.es6 b/app/assets/javascripts/create_label.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..744aa0afa03e84aa9e0383c795c8c2e9410768dd
--- /dev/null
+++ b/app/assets/javascripts/create_label.js.es6
@@ -0,0 +1,130 @@
+/* eslint-disable */
+(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 = label.message.map(function (value, key) {
+              return key + " " + value[0];
+            }).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/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..331f0209888770f26302832f5387a7d5b3933fac
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -0,0 +1,98 @@
+/* eslint-disable */
+//= require vue
+
+((global) => {
+
+  const COOKIE_NAME = 'cycle_analytics_help_dismissed';
+  const store = gl.cycleAnalyticsStore = {
+    isLoading: true,
+    hasError: false,
+    isHelpDismissed: Cookies.get(COOKIE_NAME),
+    analytics: {}
+  };
+
+  gl.CycleAnalytics = class CycleAnalytics {
+    constructor() {
+      const that = this;
+
+      this.vue = new Vue({
+        el: '#cycle-analytics',
+        name: 'CycleAnalytics',
+        created: this.fetchData(),
+        data: store,
+        methods: {
+          dismissLanding() {
+            that.dismissLanding();
+          }
+        }
+      });
+    }
+
+    fetchData(options) {
+      store.isLoading = true;
+      options = options || { startDate: 30 };
+
+      $.ajax({
+        url: $('#cycle-analytics').data('request-path'),
+        method: 'GET',
+        dataType: 'json',
+        contentType: 'application/json',
+        data: {
+          cycle_analytics: {
+            start_date: options.startDate
+          }
+        }
+      }).done((data) => {
+        this.decorateData(data);
+        this.initDropdown();
+      })
+      .error((data) => {
+        this.handleError(data);
+      })
+      .always(() => {
+        store.isLoading = false;
+      })
+    }
+
+    decorateData(data) {
+      data.summary = data.summary || [];
+      data.stats = data.stats || [];
+
+      data.summary.forEach((item) => {
+        item.value = item.value || '-';
+      });
+
+      data.stats.forEach((item) => {
+        item.value = item.value || '- - -';
+      });
+
+      store.analytics = data;
+    }
+
+    handleError(data) {
+      store.hasError = true;
+      new Flash('There was an error while fetching cycle analytics data.', 'alert');
+    }
+
+    dismissLanding() {
+      store.isHelpDismissed = true;
+      Cookies.set(COOKIE_NAME, true);
+    }
+
+    initDropdown() {
+      const $dropdown = $('.js-ca-dropdown');
+      const $label = $dropdown.find('.dropdown-label');
+
+      $dropdown.find('li a').off('click').on('click', (e) => {
+        e.preventDefault();
+        const $target = $(e.currentTarget);
+        const value = $target.data('value');
+
+        $label.text($target.text().trim());
+        this.fetchData({ startDate: value });
+      })
+    }
+
+  }
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 3dd7ceba92faaa61683d2304f521d12274f1b207..82bfdcea0ca9da9d59ecc3282b388bb5e474f42e 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Diff = (function() {
     var UNFOLD_COUNT;
@@ -7,6 +8,9 @@
     function Diff() {
       $('.files .diff-file').singleFileDiff();
       this.filesCommentButton = $('.files .diff-file').filesCommentButton();
+      if (this.diffViewType() === 'parallel') {
+        $('.content-wrapper .container-fluid').removeClass('container-limited');
+      }
       $(document).off('click', '.js-unfold');
       $(document).on('click', '.js-unfold', (function(_this) {
         return function(event) {
@@ -39,7 +43,6 @@
             bottom: unfoldBottom,
             offset: offset,
             unfold: unfold,
-            indent: 1,
             view: file.data('view')
           };
           return $.get(link, params, function(response) {
@@ -49,6 +52,10 @@
       })(this));
     }
 
+    Diff.prototype.diffViewType = function() {
+      return $('.inline-parallel-buttons a.active').data('view-type');
+    }
+
     Diff.prototype.lineNumbers = function(line) {
       if (!line.children().length) {
         return [0, 0];
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..29a12a2395b93521dc3f6d54c16ac304fe18bd08
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -0,0 +1,50 @@
+/* eslint-disable */
+((w) => {
+  w.CommentAndResolveBtn = Vue.extend({
+    props: {
+      discussionId: String,
+      textareaIsEmpty: Boolean
+    },
+    computed: {
+      discussion: function () {
+        return CommentsStore.state[this.discussionId];
+      },
+      showButton: function () {
+        if (this.discussion) {
+          return this.discussion.isResolvable();
+        } else {
+          return false;
+        }
+      },
+      isDiscussionResolved: function () {
+        return this.discussion.isResolved();
+      },
+      buttonText: function () {
+        if (this.isDiscussionResolved) {
+          if (this.textareaIsEmpty) {
+            return "Unresolve discussion";
+          } else {
+            return "Comment & unresolve discussion";
+          }
+        } else {
+          if (this.textareaIsEmpty) {
+            return "Resolve discussion";
+          } else {
+            return "Comment & resolve discussion";
+          }
+        }
+      }
+    },
+    ready: function () {
+      const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+      this.textareaIsEmpty = $textarea.val() === '';
+
+      $textarea.on('input.comment-and-resolve-btn', () => {
+        this.textareaIsEmpty = $textarea.val() === '';
+      });
+    },
+    destroyed: function () {
+      $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+    }
+  });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..983e554b9c1b69853fcfe3ec6f67405dddb03c35
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -0,0 +1,189 @@
+/* eslint-disable */
+(() => {
+  JumpToDiscussion = Vue.extend({
+    mixins: [DiscussionMixins],
+    props: {
+      discussionId: String
+    },
+    data: function () {
+      return {
+        discussions: CommentsStore.state,
+      };
+    },
+    computed: {
+      discussion: function () {
+        return this.discussions[this.discussionId];
+      },
+      allResolved: function () {
+        return this.unresolvedDiscussionCount === 0;
+      },
+      showButton: function () {
+        if (this.discussionId) {
+          if (this.unresolvedDiscussionCount > 1) {
+            return true;
+          } else {
+            return this.discussionId !== this.lastResolvedId;
+          }
+        } else {
+          return this.unresolvedDiscussionCount >= 1;
+        }
+      },
+      lastResolvedId: function () {
+        let lastId;
+        for (const discussionId in this.discussions) {
+          const discussion = this.discussions[discussionId];
+
+          if (!discussion.isResolved()) {
+            lastId = discussion.id;
+          }
+        }
+        return lastId;
+      }
+    },
+    methods: {
+      jumpToNextUnresolvedDiscussion: function () {
+        let discussionsSelector,
+            discussionIdsInScope,
+            firstUnresolvedDiscussionId,
+            nextUnresolvedDiscussionId,
+            activeTab = window.mrTabs.currentAction,
+            hasDiscussionsToJumpTo = true,
+            jumpToFirstDiscussion = !this.discussionId;
+
+        const discussionIdsForElements = function(elements) {
+          return elements.map(function() {
+            return $(this).attr('data-discussion-id');
+          }).toArray();
+        };
+
+        const discussions = this.discussions;
+
+        if (activeTab === 'diffs') {
+          discussionsSelector = '.diffs .notes[data-discussion-id]';
+          discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+          let unresolvedDiscussionCount = 0;
+
+          for (let i = 0; i < discussionIdsInScope.length; i++) {
+            const discussionId = discussionIdsInScope[i];
+            const discussion = discussions[discussionId];
+            if (discussion && !discussion.isResolved()) {
+              unresolvedDiscussionCount++;
+            }
+          }
+
+          if (this.discussionId && !this.discussion.isResolved()) {
+            // If this is the last unresolved discussion on the diffs tab,
+            // there are no discussions to jump to.
+            if (unresolvedDiscussionCount === 1) {
+              hasDiscussionsToJumpTo = false;
+            }
+          } else {
+            // If there are no unresolved discussions on the diffs tab at all,
+            // there are no discussions to jump to.
+            if (unresolvedDiscussionCount === 0) {
+              hasDiscussionsToJumpTo = false;
+            }
+          }
+        } else if (activeTab !== 'notes') {
+          // If we are on the commits or builds tabs,
+          // there are no discussions to jump to.
+          hasDiscussionsToJumpTo = false;
+        }
+
+        if (!hasDiscussionsToJumpTo) {
+          // If there are no discussions to jump to on the current page,
+          // switch to the notes tab and jump to the first disucssion there.
+          window.mrTabs.activateTab('notes');
+          activeTab = 'notes';
+          jumpToFirstDiscussion = true;
+        }
+
+        if (activeTab === 'notes') {
+          discussionsSelector = '.discussion[data-discussion-id]';
+          discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+        }
+
+        let currentDiscussionFound = false;
+        for (let i = 0; i < discussionIdsInScope.length; i++) {
+          const discussionId = discussionIdsInScope[i];
+          const discussion = discussions[discussionId];
+
+          if (!discussion) {
+            // Discussions for comments on commits in this MR don't have a resolved status.
+            continue;
+          }
+
+          if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+            firstUnresolvedDiscussionId = discussionId;
+
+            if (jumpToFirstDiscussion) {
+              break;
+            }
+          }
+
+          if (!jumpToFirstDiscussion) {
+            if (currentDiscussionFound) {
+              if (!discussion.isResolved()) {
+                nextUnresolvedDiscussionId = discussionId;
+                break;
+              }
+              else {
+                continue;
+              }
+            }
+
+            if (discussionId === this.discussionId) {
+              currentDiscussionFound = true;
+            }
+          }
+        }
+
+        nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+
+        if (!nextUnresolvedDiscussionId) {
+          return;
+        }
+
+        let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+
+        if (activeTab === 'notes') {
+          $target = $target.closest('.note-discussion');
+
+          // If the next discussion is closed, toggle it open.
+          if ($target.find('.js-toggle-content').is(':hidden')) {
+            $target.find('.js-toggle-button i').trigger('click')
+          }
+        } else if (activeTab === 'diffs') {
+          // Resolved discussions are hidden in the diffs tab by default.
+          // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+          // When jumping between unresolved discussions on the diffs tab, we show them.
+          $target.closest(".content").show();
+
+          $target = $target.closest("tr.notes_holder");
+          $target.show();
+
+          // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+          // 4 diff lines above it: the line the discussion was in response to + 3 context
+          let prevEl;
+          for (let i = 0; i < 4; i++) {
+            prevEl = $target.prev();
+
+            // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+            if (!prevEl.hasClass("line_holder")) {
+              break;
+            }
+
+            $target = prevEl;
+          }
+        }
+
+        $.scrollTo($target, {
+          offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+        });
+      }
+    }
+  });
+
+  Vue.component('jump-to-discussion', JumpToDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..bcc052c7c8c4818f29dd3306a96ff97cddd75691
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -0,0 +1,104 @@
+/* eslint-disable */
+((w) => {
+  w.ResolveBtn = Vue.extend({
+    props: {
+      noteId: Number,
+      discussionId: String,
+      resolved: Boolean,
+      projectPath: String,
+      canResolve: Boolean,
+      resolvedBy: String
+    },
+    data: function () {
+      return {
+        discussions: CommentsStore.state,
+        loading: false
+      };
+    },
+    watch: {
+      'discussions': {
+        handler: 'updateTooltip',
+        deep: true
+      }
+    },
+    computed: {
+      discussion: function () {
+        return this.discussions[this.discussionId];
+      },
+      note: function () {
+        if (this.discussion) {
+          return this.discussion.getNote(this.noteId);
+        } else {
+          return undefined;
+        }
+      },
+      buttonText: function () {
+        if (this.isResolved) {
+          return `Resolved by ${this.resolvedByName}`;
+        } else if (this.canResolve) {
+          return 'Mark as resolved';
+        } else {
+          return 'Unable to resolve';
+        }
+      },
+      isResolved: function () {
+        if (this.note) {
+          return this.note.resolved;
+        } else {
+          return false;
+        }
+      },
+      resolvedByName: function () {
+        return this.note.resolved_by;
+      },
+    },
+    methods: {
+      updateTooltip: function () {
+        $(this.$els.button)
+          .tooltip('hide')
+          .tooltip('fixTitle');
+      },
+      resolve: function () {
+        if (!this.canResolve) return;
+
+        let promise;
+        this.loading = true;
+
+        if (this.isResolved) {
+          promise = ResolveService
+            .unresolve(this.projectPath, this.noteId);
+        } else {
+          promise = ResolveService
+            .resolve(this.projectPath, this.noteId);
+        }
+
+        promise.then((response) => {
+          this.loading = false;
+
+          if (response.status === 200) {
+            const data = response.json();
+            const resolved_by = data ? data.resolved_by : null;
+
+            CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+            this.discussion.updateHeadline(data);
+          } else {
+            new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
+          }
+
+          this.$nextTick(this.updateTooltip);
+        });
+      }
+    },
+    compiled: function () {
+      $(this.$els.button).tooltip({
+        container: 'body'
+      });
+    },
+    beforeDestroy: function () {
+      CommentsStore.delete(this.discussionId, this.noteId);
+    },
+    created: function () {
+      CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+    }
+  });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..24a99e23132b696721ed31de9c8d5de569203ec3
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -0,0 +1,19 @@
+/* eslint-disable */
+((w) => {
+  w.ResolveCount = Vue.extend({
+    mixins: [DiscussionMixins],
+    props: {
+      loggedOut: Boolean
+    },
+    data: function () {
+      return {
+        discussions: CommentsStore.state
+      };
+    },
+    computed: {
+      allResolved: function () {
+        return this.resolvedDiscussionCount === this.discussionCount;
+      }
+    }
+  });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..060034f049b9066e027eb7252501278ab0650ca3
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -0,0 +1,57 @@
+/* eslint-disable */
+((w) => {
+  w.ResolveDiscussionBtn = Vue.extend({
+    props: {
+      discussionId: String,
+      mergeRequestId: Number,
+      projectPath: String,
+      canResolve: Boolean,
+    },
+    data: function() {
+      return {
+        discussions: CommentsStore.state
+      };
+    },
+    computed: {
+      discussion: function () {
+        return this.discussions[this.discussionId];
+      },
+      showButton: function () {
+        if (this.discussion) {
+          return this.discussion.isResolvable();
+        } else {
+          return false;
+        }
+      },
+      isDiscussionResolved: function () {
+        if (this.discussion) {
+          return this.discussion.isResolved();
+        } else {
+          return false;
+        }
+      },
+      buttonText: function () {
+        if (this.isDiscussionResolved) {
+          return "Unresolve discussion";
+        } else {
+          return "Resolve discussion";
+        }
+      },
+      loading: function () {
+        if (this.discussion) {
+          return this.discussion.loading;
+        } else {
+          return false;
+        }
+      }
+    },
+    methods: {
+      resolve: function () {
+        ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
+      }
+    },
+    created: function () {
+      CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+    }
+  });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..6149bfd052a1b64110158125f3a6bae8a8654226
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -0,0 +1,36 @@
+/* eslint-disable */
+//= require vue
+//= require vue-resource
+//= require_directory ./models
+//= require_directory ./stores
+//= require_directory ./services
+//= require_directory ./mixins
+//= require_directory ./components
+
+$(() => {
+  window.DiffNotesApp = new Vue({
+    el: '#diff-notes-app',
+    components: {
+      'resolve-btn': ResolveBtn,
+      'resolve-discussion-btn': ResolveDiscussionBtn,
+      'comment-and-resolve-btn': CommentAndResolveBtn
+    },
+    methods: {
+      compileComponents: function () {
+        const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
+        if ($components.length) {
+          $components.each(function () {
+            DiffNotesApp.$compile($(this).get(0));
+          });
+        }
+      }
+    }
+  });
+
+  new Vue({
+    el: '#resolve-count-app',
+    components: {
+      'resolve-count': ResolveCount
+    }
+  });
+});
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..7a929017f36a00e706c98262a52273d1b95a3f5c
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -0,0 +1,36 @@
+/* eslint-disable */
+((w) => {
+  w.DiscussionMixins = {
+    computed: {
+      discussionCount: function () {
+        return Object.keys(this.discussions).length;
+      },
+      resolvedDiscussionCount: function () {
+        let resolvedCount = 0;
+
+        for (const discussionId in this.discussions) {
+          const discussion = this.discussions[discussionId];
+
+          if (discussion.isResolved()) {
+            resolvedCount++;
+          }
+        }
+
+        return resolvedCount;
+      },
+      unresolvedDiscussionCount: function () {
+        let unresolvedCount = 0;
+
+        for (const discussionId in this.discussions) {
+          const discussion = this.discussions[discussionId];
+
+          if (!discussion.isResolved()) {
+            unresolvedCount++;
+          }
+        }
+
+        return unresolvedCount;
+      }
+    }
+  };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..439f55520ef6922f4d21d3235a997d4596a8b54e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -0,0 +1,88 @@
+/* eslint-disable */
+class DiscussionModel {
+  constructor (discussionId) {
+    this.id = discussionId;
+    this.notes = {};
+    this.loading = false;
+    this.canResolve = false;
+  }
+
+  createNote (noteId, canResolve, resolved, resolved_by) {
+    Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+  }
+
+  deleteNote (noteId) {
+    Vue.delete(this.notes, noteId);
+  }
+
+  getNote (noteId) {
+    return this.notes[noteId];
+  }
+
+  notesCount() {
+    return Object.keys(this.notes).length;
+  }
+
+  isResolved () {
+    for (const noteId in this.notes) {
+      const note = this.notes[noteId];
+
+      if (!note.resolved) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  resolveAllNotes (resolved_by) {
+    for (const noteId in this.notes) {
+      const note = this.notes[noteId];
+
+      if (!note.resolved) {
+        note.resolved = true;
+        note.resolved_by = resolved_by;
+      }
+    }
+  }
+
+  unResolveAllNotes () {
+    for (const noteId in this.notes) {
+      const note = this.notes[noteId];
+
+      if (note.resolved) {
+        note.resolved = false;
+        note.resolved_by = null;
+      }
+    }
+  }
+
+  updateHeadline (data) {
+    const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
+
+    if (data.discussion_headline_html) {
+      if ($discussionHeadline.length) {
+        $discussionHeadline.replaceWith(data.discussion_headline_html);
+      } else {
+        $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
+      }
+    } else {
+       $discussionHeadline.remove();
+    }
+  }
+
+  isResolvable () {
+    if (!this.canResolve) {
+      return false;
+    }
+    
+    for (const noteId in this.notes) {
+      const note = this.notes[noteId];
+
+      if (note.canResolve) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..d0541b0263278efe32e105b30d6312b26148f770
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -0,0 +1,10 @@
+/* eslint-disable */
+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;
+  }
+}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..86953ce7ffb2df158bfc262d4c3f83a284b16861
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -0,0 +1,89 @@
+/* eslint-disable */
+((w) => {
+  class ResolveServiceClass {
+    constructor() {
+      this.noteResource = Vue.resource('notes{/noteId}/resolve');
+      this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
+    }
+
+    setCSRF() {
+      Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
+    }
+
+    prepareRequest(root) {
+      this.setCSRF();
+      Vue.http.options.root = root;
+    }
+
+    resolve(projectPath, noteId) {
+      this.prepareRequest(projectPath);
+
+      return this.noteResource.save({ noteId }, {});
+    }
+
+    unresolve(projectPath, noteId) {
+      this.prepareRequest(projectPath);
+
+      return this.noteResource.delete({ noteId }, {});
+    }
+
+    toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId],
+            isResolved = discussion.isResolved();
+      let promise;
+
+      if (isResolved) {
+        promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
+      } else {
+        promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
+      }
+
+      promise.then((response) => {
+        discussion.loading = false;
+
+        if (response.status === 200) {
+          const data = response.json();
+          const resolved_by = data ? data.resolved_by : null;
+
+          if (isResolved) {
+            discussion.unResolveAllNotes();
+          } else {
+            discussion.resolveAllNotes(resolved_by);
+          }
+
+          discussion.updateHeadline(data);
+        } else {
+          new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+        }
+      })
+    }
+
+    resolveAll(projectPath, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId];
+
+      this.prepareRequest(projectPath);
+
+      discussion.loading = true;
+
+      return this.discussionResource.save({
+        mergeRequestId,
+        discussionId
+      }, {});
+    }
+
+    unResolveAll(projectPath, mergeRequestId, discussionId) {
+      const discussion = CommentsStore.state[discussionId];
+
+      this.prepareRequest(projectPath);
+
+      discussion.loading = true;
+
+      return this.discussionResource.delete({
+        mergeRequestId,
+        discussionId
+      }, {});
+    }
+  }
+
+  w.ResolveService = new ResolveServiceClass();
+})(window);
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..f42ca406bb1df2779b5a761043dcedd7e9495cbc
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -0,0 +1,54 @@
+/* eslint-disable */
+((w) => {
+  w.CommentsStore = {
+    state: {},
+    get: function (discussionId, noteId) {
+      return this.state[discussionId].getNote(noteId);
+    },
+    createDiscussion: function (discussionId, canResolve) {
+      let discussion = this.state[discussionId];
+      if (!this.state[discussionId]) {
+        discussion = new DiscussionModel(discussionId);
+        Vue.set(this.state, discussionId, discussion);
+      }
+
+      if (canResolve !== undefined) {
+        discussion.canResolve = canResolve;
+      }
+
+      return discussion;
+    },
+    create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
+      const discussion = this.createDiscussion(discussionId);
+
+      discussion.createNote(noteId, canResolve, resolved, resolved_by);
+    },
+    update: function (discussionId, noteId, resolved, resolved_by) {
+      const discussion = this.state[discussionId];
+      const note = discussion.getNote(noteId);
+      note.resolved = resolved;
+      note.resolved_by = resolved_by;
+    },
+    delete: function (discussionId, noteId) {
+      const discussion = this.state[discussionId];
+      discussion.deleteNote(noteId);
+
+      if (discussion.notesCount() === 0) {
+        Vue.delete(this.state, discussionId);
+      }
+    },
+    unresolvedDiscussionIds: function () {
+      let ids = [];
+
+      for (const discussionId in this.state) {
+        const discussion = this.state[discussionId];
+
+        if (!discussion.isResolved()) {
+          ids.push(discussion.id);
+        }
+      }
+
+      return ids;
+    }
+  };
+})(window);
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6
similarity index 80%
rename from app/assets/javascripts/dispatcher.js
rename to app/assets/javascripts/dispatcher.js.es6
index 7160fa71ce5638d40c2cc7a14140567ddde2b7f9..756a24cc0fc00e8a8928f512dee114e17dd2a889 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var Dispatcher;
 
@@ -8,6 +9,7 @@
   Dispatcher = (function() {
     function Dispatcher() {
       this.initSearch();
+      this.initFieldErrors();
       this.initPageScripts();
     }
 
@@ -20,9 +22,20 @@
       path = page.split(':');
       shortcut_handler = null;
       switch (page) {
+        case 'sessions:new':
+          new UsernameValidator();
+          break;
+        case 'projects:boards:show':
+        case 'projects:boards:index':
+          shortcut_handler = new ShortcutsNavigation();
+          break;
+        case 'projects:builds:show':
+          new Build();
+          break;
+        case 'projects:merge_requests:index':
         case 'projects:issues:index':
           Issuable.init();
-          new IssuableBulkActions();
+          new gl.IssuableBulkActions();
           shortcut_handler = new ShortcutsNavigation();
           break;
         case 'projects:issues:show':
@@ -36,12 +49,12 @@
           new Milestone();
           break;
         case 'dashboard:todos:index':
-          new Todos();
+          new gl.Todos();
           break;
         case 'projects:milestones:new':
         case 'projects:milestones:edit':
           new ZenMode();
-          new DueDateSelect();
+          new gl.DueDateSelectors();
           new GLForm($('.milestone-form'));
           break;
         case 'groups:milestones:new':
@@ -55,7 +68,9 @@
           shortcut_handler = new ShortcutsNavigation();
           new GLForm($('.issue-form'));
           new IssuableForm($('.issue-form'));
-          new IssuableTemplateSelectors();
+          new LabelsSelect();
+          new MilestoneSelect();
+          new gl.IssuableTemplateSelectors();
           break;
         case 'projects:merge_requests:new':
         case 'projects:merge_requests:edit':
@@ -63,7 +78,9 @@
           shortcut_handler = new ShortcutsNavigation();
           new GLForm($('.merge-request-form'));
           new IssuableForm($('.merge-request-form'));
-          new IssuableTemplateSelectors();
+          new LabelsSelect();
+          new MilestoneSelect();
+          new gl.IssuableTemplateSelectors();
           break;
         case 'projects:tags:new':
           new ZenMode();
@@ -104,6 +121,9 @@
           new ZenMode();
           shortcut_handler = new ShortcutsNavigation();
           break;
+        case 'projects:commit:builds':
+          new gl.Pipelines();
+          break;
         case 'projects:commits:show':
         case 'projects:activity':
           shortcut_handler = new ShortcutsNavigation();
@@ -115,6 +135,9 @@
             new TreeView();
           }
           break;
+        case 'projects:pipelines:show':
+          new gl.Pipelines();
+          break;
         case 'groups:activity':
           new Activities();
           break;
@@ -124,11 +147,13 @@
           new NotificationsDropdown();
           break;
         case 'groups:group_members:index':
-          new GroupMembers();
+          new gl.MemberExpirationDate();
+          new gl.Members();
           new UsersSelect();
           break;
         case 'projects:project_members:index':
-          new ProjectMembers();
+          new gl.MemberExpirationDate();
+          new gl.Members();
           new UsersSelect();
           break;
         case 'groups:new':
@@ -150,16 +175,20 @@
           shortcut_handler = new ShortcutsNavigation();
           new ShortcutsBlob(true);
           break;
+        case 'groups:labels:new':
+        case 'groups:labels:edit':
         case 'projects:labels:new':
         case 'projects:labels:edit':
           new Labels();
           break;
         case 'projects:labels:index':
           if ($('.prioritized-labels').length) {
-            new LabelManager();
+            new gl.LabelManager();
           }
           break;
         case 'projects:network:show':
+          // Ensure we don't create a particular shortcut handler here. This is
+          // already created, where the network graph is created.
           shortcut_handler = true;
           break;
         case 'projects:forks:new':
@@ -169,6 +198,7 @@
           new BuildArtifacts();
           break;
         case 'projects:group_links:index':
+          new gl.MemberExpirationDate();
           new GroupsSelect();
           break;
         case 'search:show':
@@ -178,6 +208,9 @@
           new gl.ProtectedBranchCreate();
           new gl.ProtectedBranchEditList();
           break;
+        case 'projects:cycle_analytics:show':
+          new gl.CycleAnalytics();
+          break;
       }
       switch (path.first()) {
         case 'admin':
@@ -191,9 +224,13 @@
               break;
             case 'labels':
               switch (path[2]) {
+                case 'new':
                 case 'edit':
                   new Labels();
               }
+            case 'abuse_reports':
+              new gl.AbuseReports();
+              break;
           }
           break;
         case 'dashboard':
@@ -251,17 +288,25 @@
               shortcut_handler = new ShortcutsNavigation();
           }
       }
+      // If we haven't installed a custom shortcut handler, install the default one
       if (!shortcut_handler) {
         return new Shortcuts();
       }
     };
 
     Dispatcher.prototype.initSearch = function() {
+      // Only when search form is present
       if ($('.search').length) {
-        return new SearchAutocomplete();
+        return new gl.SearchAutocomplete();
       }
     };
 
+    Dispatcher.prototype.initFieldErrors = function() {
+      $('.gl-show-field-errors').each((i, form) => {
+        new gl.GlFieldErrors(form);
+      });
+    };
+
     return Dispatcher;
 
   })();
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 4a6fea929c758b55d5fa382d868e6982710258d1..1a0aa9757ba9b9f6bfb716d3fe9896d0f71b578c 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require preview_markdown */
 
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
deleted file mode 100644
index 5a725a41fd123f9d4084136f621dde967c8d4b4a..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/due_date_select.js
+++ /dev/null
@@ -1,104 +0,0 @@
-(function() {
-  this.DueDateSelect = (function() {
-    function DueDateSelect() {
-      var $datePicker, $dueDate, $loading;
-      $datePicker = $('.datepicker');
-      if ($datePicker.length) {
-        $dueDate = $('#milestone_due_date');
-        $datePicker.datepicker({
-          dateFormat: 'yy-mm-dd',
-          onSelect: function(dateText, inst) {
-            return $dueDate.val(dateText);
-          }
-        }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
-      }
-      $('.js-clear-due-date').on('click', function(e) {
-        e.preventDefault();
-        return $.datepicker._clearDate($datePicker);
-      });
-      $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
-      $('.js-due-date-select').each(function(i, dropdown) {
-        var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
-        $dropdown = $(dropdown);
-        $dropdownParent = $dropdown.closest('.dropdown');
-        $datePicker = $dropdownParent.find('.js-due-date-calendar');
-        $block = $dropdown.closest('.block');
-        $selectbox = $dropdown.closest('.selectbox');
-        $value = $block.find('.value');
-        $valueContent = $block.find('.value-content');
-        $sidebarValue = $('.js-due-date-sidebar-value', $block);
-        fieldName = $dropdown.data('field-name');
-        abilityName = $dropdown.data('ability-name');
-        issueUpdateURL = $dropdown.data('issue-update');
-        $dropdown.glDropdown({
-          hidden: function() {
-            $selectbox.hide();
-            return $value.css('display', '');
-          }
-        });
-        addDueDate = function(isDropdown) {
-          var data, date, mediumDate, value;
-          value = $("input[name='" + fieldName + "']").val();
-          if (value !== '') {
-            date = new Date(value.replace(new RegExp('-', 'g'), ','));
-            mediumDate = $.datepicker.formatDate('M d, yy', date);
-          } else {
-            mediumDate = 'No due date';
-          }
-          data = {};
-          data[abilityName] = {};
-          data[abilityName].due_date = value;
-          return $.ajax({
-            type: 'PUT',
-            url: issueUpdateURL,
-            data: data,
-            dataType: 'json',
-            beforeSend: function() {
-              var cssClass;
-              $loading.fadeIn();
-              if (isDropdown) {
-                $dropdown.trigger('loading.gl.dropdown');
-                $selectbox.hide();
-              }
-              $value.css('display', '');
-              cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value';
-              $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>");
-              $sidebarValue.html(mediumDate);
-              if (value !== '') {
-                return $('.js-remove-due-date-holder').removeClass('hidden');
-              } else {
-                return $('.js-remove-due-date-holder').addClass('hidden');
-              }
-            }
-          }).done(function(data) {
-            if (isDropdown) {
-              $dropdown.trigger('loaded.gl.dropdown');
-              $dropdown.dropdown('toggle');
-            }
-            return $loading.fadeOut();
-          });
-        };
-        $block.on('click', '.js-remove-due-date', function(e) {
-          e.preventDefault();
-          $("input[name='" + fieldName + "']").val('');
-          return addDueDate(false);
-        });
-        return $datePicker.datepicker({
-          dateFormat: 'yy-mm-dd',
-          defaultDate: $("input[name='" + fieldName + "']").val(),
-          altField: "input[name='" + fieldName + "']",
-          onSelect: function() {
-            return addDueDate(true);
-          }
-        });
-      });
-      $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) {
-        return e.stopImmediatePropagation();
-      });
-    }
-
-    return DueDateSelect;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..fd7f961aab9804f3f3296b83fd3b171b5ed29cb7
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -0,0 +1,185 @@
+/* eslint-disable */
+(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();
+      this.initStopPropagation();
+    }
+
+    initGlDropdown() {
+      this.$dropdown.glDropdown({
+        hidden: () => {
+          this.$selectbox.hide();
+          this.$value.css('display', '');
+        }
+      });
+    }
+
+    initDatePicker() {
+      this.$datePicker.datepicker({
+        dateFormat: 'yy-mm-dd',
+        defaultDate: $("input[name='" + this.fieldName + "']").val(),
+        altField: "input[name='" + this.fieldName + "']",
+        onSelect: () => {
+          if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+            gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
+            this.updateIssueBoardIssue();
+          } else {
+            return this.saveDueDate(true);
+          }
+        }
+      });
+    }
+
+    initRemoveDueDate() {
+      this.$block.on('click', '.js-remove-due-date', (e) => {
+        e.preventDefault();
+
+        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);
+        }
+      });
+    }
+
+    initStopPropagation() {
+      $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
+        return e.stopImmediatePropagation();
+      });
+    }
+
+    saveDueDate(isDropdown) {
+      this.parseSelectedDate();
+      this.prepSelectedDate();
+      this.submitSelectedDate(isDropdown);
+    }
+
+    parseSelectedDate() {
+      this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
+      if (this.rawSelectedDate.length) {
+        let dateObj = new Date(this.rawSelectedDate);
+        this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
+      } 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.initMilestoneDueDate();
+      this.initIssuableSelect();
+    }
+
+    initMilestoneDueDate() {
+      const $datePicker = $('.datepicker');
+
+      if ($datePicker.length) {
+        const $dueDate = $('#milestone_due_date');
+        $datePicker.datepicker({
+          dateFormat: 'yy-mm-dd',
+          onSelect: (dateText, inst) => {
+            $dueDate.val(dateText);
+          }
+        }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
+      }
+      $('.js-clear-due-date').on('click', (e) => {
+        e.preventDefault();
+        $.datepicker._clearDate($datePicker);
+      });
+    }
+
+    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/extensions/array.js b/app/assets/javascripts/extensions/array.js
index 24f9e00097cb51e7aa20d9ac0b374f7393038492..4c9e219aa43f8281ed5445131a848d47cb49fa3f 100644
--- a/app/assets/javascripts/extensions/array.js
+++ b/app/assets/javascripts/extensions/array.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 Array.prototype.first = function() {
   return this[0];
 }
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..afb2f0d69563af62843820961794bdf9e390674d
--- /dev/null
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -0,0 +1,9 @@
+/* global Element */
+/* eslint-disable consistent-return, max-len */
+
+Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
+
+Element.prototype.closest = function closest(selector, selectedElement = this) {
+  if (!selectedElement) return;
+  return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
+};
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index ae3dde63da382bfb6cb24695daeb34579f1c5c2f..623a80b705339bb5e35b96028dc0ba5ef53277ef 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,3 +1,5 @@
+/* eslint-disable */
+// Disable an element and add the 'disabled' Bootstrap class
 (function() {
   $.fn.extend({
     disable: function() {
@@ -5,6 +7,7 @@
     }
   });
 
+  // Enable an element and remove the 'disabled' Bootstrap class
   $.fn.extend({
     enable: function() {
       return $(this).removeAttr('disabled').removeClass('disabled');
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index b2e49b71fec2eb0b5a33142b3dcb48e7d4718666..732136f1f2c63b251c8cd3cb03a845bba73b8cfa 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -39,12 +40,13 @@
     FilesCommentButton.prototype.render = function(e) {
       var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
       $currentTarget = $(e.currentTarget);
+
       buttonParentElement = this.getButtonParent($currentTarget);
-      if (!this.shouldRender(e, buttonParentElement)) {
-        return;
-      }
-      textFileElement = this.getTextFileElement($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'),
@@ -119,10 +121,14 @@
       return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
     };
 
-    FilesCommentButton.prototype.shouldRender = function(e, buttonParentElement) {
+    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') !== '';
+    };
+
     return FilesCommentButton;
 
   })();
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index c8a02d6fa158e3de137f596ea8860f1e946ca3ac..46e272c3311a2f8908948c7af93bdc5a01e93981 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Flash = (function() {
     var hideFlash;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js.es6
similarity index 64%
rename from app/assets/javascripts/gfm_auto_complete.js
rename to app/assets/javascripts/gfm_auto_complete.js.es6
index 2e5b15f4b77ea4c512c52ff4ee816c3fac861c60..e72e2194be8c1c47a260ef40629070e6c099ac6a 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,3 +1,5 @@
+/* eslint-disable */
+// Creates the variables for setting up GFM auto-completion
 (function() {
   if (window.GitLab == null) {
     window.GitLab = {};
@@ -8,18 +10,22 @@
     dataLoaded: false,
     cachedData: {},
     dataSource: '',
+    // Emoji
     Emoji: {
       template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
     },
+    // Team Members
     Members: {
       template: '<li>${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>'
     },
@@ -28,6 +34,8 @@
     },
     DefaultOptions: {
       sorter: function(query, items, searchKey) {
+        // Highlight first item only if at least one char was typed
+        this.setting.highlightFirst = query.length > 0;
         if ((items[0].name != null) && items[0].name === 'loading') {
           return items;
         }
@@ -47,30 +55,29 @@
         }
       }
     },
-    setup: function(input) {
+    setup: _.debounce(function(input) {
+      // Add GFM auto-completion to all input fields, that accept GFM input.
       this.input = input || $('.js-gfm-input');
+      // destroy previous instances
       this.destroyAtWho();
+      // set up instances
       this.setupAtWho();
-      if (this.dataSource) {
-        if (!this.dataLoading && !this.cachedData) {
-          this.dataLoading = true;
-          setTimeout((function(_this) {
-            return function() {
-              var fetch;
-              fetch = _this.fetchData(_this.dataSource);
-              return fetch.done(function(data) {
-                _this.dataLoading = false;
-                return _this.loadData(data);
-              });
-            };
-          })(this), 1000);
-        }
-        if (this.cachedData != null) {
-          return this.loadData(this.cachedData);
-        }
+
+      if (this.dataSource && !this.dataLoading && !this.cachedData) {
+        this.dataLoading = true;
+        return this.fetchData(this.dataSource)
+          .done((data) => {
+            this.dataLoading = false;
+            this.loadData(data);
+          });
+        };
+
+      if (this.cachedData != null) {
+        return this.loadData(this.cachedData);
       }
-    },
+    }, 1000),
     setupAtWho: function() {
+      // Emoji
       this.input.atwho({
         at: ':',
         displayTpl: (function(_this) {
@@ -90,6 +97,7 @@
           beforeInsert: this.DefaultOptions.beforeInsert
         }
       });
+      // Team Members
       this.input.atwho({
         at: '@',
         displayTpl: (function(_this) {
@@ -120,8 +128,8 @@
               }
               return {
                 username: m.username,
-                title: sanitize(title),
-                search: sanitize(m.username + " " + m.name)
+                title: gl.utils.sanitize(title),
+                search: gl.utils.sanitize(m.username + " " + m.name)
               };
             });
           }
@@ -153,7 +161,7 @@
               }
               return {
                 id: i.iid,
-                title: sanitize(i.title),
+                title: gl.utils.sanitize(i.title),
                 search: i.iid + " " + i.title
               };
             });
@@ -176,6 +184,7 @@
         insertTpl: '${atwho-at}"${title}"',
         data: ['loading'],
         callbacks: {
+          sorter: this.DefaultOptions.sorter,
           beforeSave: function(milestones) {
             return $.map(milestones, function(m) {
               if (m.title == null) {
@@ -183,7 +192,7 @@
               }
               return {
                 id: m.iid,
-                title: sanitize(m.title),
+                title: gl.utils.sanitize(m.title),
                 search: "" + m.title
               };
             });
@@ -216,27 +225,28 @@
               }
               return {
                 id: m.iid,
-                title: sanitize(m.title),
+                title: gl.utils.sanitize(m.title),
                 search: m.iid + " " + m.title
               };
             });
           }
         }
       });
-      return this.input.atwho({
+      this.input.atwho({
         at: '~',
         alias: 'labels',
         searchKey: 'search',
         displayTpl: this.Labels.template,
         insertTpl: '${atwho-at}${title}',
         callbacks: {
+          sorter: this.DefaultOptions.sorter,
           beforeSave: function(merges) {
             var sanitizeLabelTitle;
             sanitizeLabelTitle = function(title) {
               if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
-                return "\"" + (sanitize(title)) + "\"";
+                return "\"" + (gl.utils.sanitize(title)) + "\"";
               } else {
-                return sanitize(title);
+                return gl.utils.sanitize(title);
               }
             };
             return $.map(merges, function(m) {
@@ -249,6 +259,68 @@
           }
         }
       });
+      // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+      this.input.filter('[data-supports-slash-commands="true"]').atwho({
+        at: '/',
+        alias: 'commands',
+        searchKey: 'search',
+        displayTpl: function(value) {
+          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);
+        },
+        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) {
+            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;
     },
     destroyAtWho: function() {
       return this.input.atwho('destroy');
@@ -259,12 +331,22 @@
     loadData: function(data) {
       this.cachedData = data;
       this.dataLoaded = true;
+      // load members
       this.input.atwho('load', '@', data.members);
+      // load issues
       this.input.atwho('load', 'issues', data.issues);
+      // load milestones
       this.input.atwho('load', 'milestones', data.milestones);
+      // load merge requests
       this.input.atwho('load', 'mergerequests', data.mergerequests);
+      // load emojis
       this.input.atwho('load', ':', data.emojis);
+      // load labels
       this.input.atwho('load', '~', data.labels);
+      // load commands
+      this.input.atwho('load', '/', data.commands);
+      // This trigger at.js again
+      // otherwise we would be stuck with loading until the user types
       return $(':focus').trigger('keyup');
     }
   };
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d3394fae3f9c758f595e512b6c447a6026345686..98e43c4d088cffcc6d1c7daad0a86c957c0f05a3 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
     bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -21,42 +22,32 @@
       $clearButton = $inputContainer.find('.js-dropdown-input-clear');
       this.indeterminateIds = [];
       $clearButton.on('click', (function(_this) {
+        // Clear click
         return function(e) {
           e.preventDefault();
           e.stopPropagation();
-          return _this.input.val('').trigger('keyup').focus();
+          return _this.input.val('').trigger('input').focus();
         };
       })(this));
+      // Key events
       timeout = "";
       this.input
         .on('keydown', function (e) {
           var keyCode = e.which;
-
-          if (keyCode === 13) {
+          if (keyCode === 13 && !options.elIsInput) {
             e.preventDefault()
           }
         })
-        .on('keyup', function(e) {
-          var keyCode;
-          keyCode = e.which;
-          if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) {
-            return;
-          }
+        .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);
           }
-          if (keyCode === 13) {
-            return false;
-          }
+          // Only filter asynchronously only if option remote is set
           if (this.options.remote) {
             clearTimeout(timeout);
             return timeout = setTimeout(function() {
-              var blurField = this.shouldBlur(keyCode);
-              if (blurField && this.filterInputBlur) {
-                this.input.blur();
-              }
               return this.options.query(this.input.val(), function(data) {
                 return this.options.callback(data);
               }.bind(this));
@@ -80,11 +71,27 @@
       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) {
@@ -111,14 +118,14 @@
             matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
             if (!$el.is('.dropdown-header')) {
               if (matches.length) {
-                return $el.show();
+                return $el.show().removeClass('option-hidden');
               } else {
-                return $el.hide();
+                return $el.hide().addClass('option-hidden');
               }
             }
           });
         } else {
-          return elements.show();
+          return elements.show().removeClass('option-hidden');
         }
       }
     };
@@ -141,6 +148,7 @@
           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);
@@ -172,6 +180,7 @@
           };
         })(this)
       });
+    // Fetch the data through ajax if the data is a string
     };
 
     return GitLabDropdownRemote;
@@ -179,7 +188,7 @@
   })();
 
   GitLabDropdown = (function() {
-    var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
+    var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
 
     LOADING_CLASS = "is-loading";
 
@@ -191,10 +200,16 @@
 
     currentIndex = -1;
 
+    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 ref, ref1, ref2, ref3, searchFields, selector, self;
+      var searchFields, selector, self;
       this.el = el1;
       this.options = options;
       this.updateLabel = bind(this.updateLabel, this);
@@ -204,16 +219,27 @@
       self = this;
       selector = $(this.el).data("target");
       this.dropdown = selector != null ? $(selector) : $(this.el).parent();
-      ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true;
+      // 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,
@@ -222,16 +248,20 @@
               return function(data) {
                 _this.fullData = data;
                 _this.parseData(_this.fullData);
+                _this.focusTextInput();
                 if (_this.options.filterable && _this.filter && _this.filter.input) {
-                  return _this.filter.input.trigger('keyup');
+                  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,
@@ -240,7 +270,7 @@
           keys: searchFields,
           elements: (function(_this) {
             return function() {
-              selector = '.dropdown-content li:not(.divider)';
+              selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
               if (_this.dropdown.find('.dropdown-toggle-page').length) {
                 selector = ".dropdown-page-one " + selector;
               }
@@ -256,23 +286,29 @@
             return function(data) {
               _this.parseData(data);
               if (_this.filterInput.val() !== '') {
-                selector = '.dropdown-content li:not(.divider):visible';
+                selector = SELECTABLE_CLASSES;
                 if (_this.dropdown.find('.dropdown-toggle-page').length) {
                   selector = ".dropdown-page-one " + selector;
                 }
-                $(selector, _this.dropdown).first().find('a').addClass('is-focused');
-                return currentIndex = 0;
+                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');
           }
@@ -311,11 +347,18 @@
           if (self.options.clicked) {
             self.options.clicked(selected, $el, e);
           }
-          return $el.trigger('blur');
+
+          // Update label right after all modifications in dropdown has been done
+          if (self.options.toggleLabel) {
+            self.updateLabel(selected, $el, self);
+          }
+
+          $el.trigger('blur');
         });
       }
     }
 
+    // Finds an element inside wrapper element
     GitLabDropdown.prototype.getElement = function(selector) {
       return this.dropdown.find(selector);
     };
@@ -333,6 +376,7 @@
         }
       }
       menu.toggleClass(PAGE_TWO_CLASS);
+      // Focus first visible input on active page
       return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
     };
 
@@ -340,23 +384,28 @@
       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);
     };
@@ -376,7 +425,9 @@
       var $target;
       if (this.options.multiSelect) {
         $target = $(e.target);
-        if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+        if ($target && !$target.hasClass('dropdown-menu-close') &&
+                       !$target.hasClass('dropdown-menu-close-icon') &&
+                       !$target.data('is-link')) {
           e.stopPropagation();
           return false;
         } else {
@@ -387,36 +438,52 @@
 
     GitLabDropdown.prototype.opened = function() {
       var contentHtml;
-      currentIndex = -1;
+      this.resetRows();
       this.addArrowKeyEvent();
+
       if (this.options.setIndeterminateIds) {
         this.options.setIndeterminateIds.call(this);
       }
       if (this.options.setActiveIds) {
         this.options.setActiveIds.call(this);
       }
+      // Makes indeterminate items effective
       if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
         this.parseData(this.fullData);
       }
       contentHtml = $('.dropdown-content', this.dropdown).html();
       if (this.remote && contentHtml === "") {
         this.remote.execute();
+      } else {
+        this.focusTextInput();
       }
-      if (this.options.filterable) {
-        this.filterInput.focus();
+
+      if (this.options.showMenuAbove) {
+        this.positionMenuAbove();
       }
+
       return this.dropdown.trigger('shown.gl.dropdown');
     };
 
+    GitLabDropdown.prototype.positionMenuAbove = function() {
+      var $button = $(this.el);
+      var $menu = this.dropdown.find('.dropdown-menu');
+
+      $menu.css('top', ($button.height() + $menu.height()) * -1);
+    };
+
     GitLabDropdown.prototype.hidden = function(e) {
       var $input;
+      this.resetRows();
       this.removeArrayKeyEvent();
       $input = this.dropdown.find(".dropdown-input-field");
       if (this.options.filterable) {
         $input.blur().val("");
       }
+      // Triggering 'keyup' will re-render the dropdown which is not always required
+      // specially if we want to keep the state of the dropdown needed for bulk-assignment
       if (!this.options.persistWhenHide) {
-        $input.trigger("keyup");
+        $input.trigger("input");
       }
       if (this.dropdown.find(".dropdown-toggle-page").length) {
         $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
@@ -427,17 +494,32 @@
       return this.dropdown.trigger('hidden.gl.dropdown');
     };
 
+    // Render the full menu
     GitLabDropdown.prototype.renderMenu = function(html) {
-      var menu_html;
-      menu_html = "";
       if (this.options.renderMenu) {
-        menu_html = this.options.renderMenu(html);
+        return this.options.renderMenu(html);
       } else {
-        menu_html = $('<ul />').append(html);
+        var ul = document.createElement('ul');
+
+        for (var i = 0; i < html.length; i++) {
+          var el = html[i];
+
+          if (el instanceof jQuery) {
+            el = el.get(0);
+          }
+
+          if (typeof el === 'string') {
+            ul.innerHTML += el;
+          } else {
+            ul.appendChild(el);
+          }
+        }
+
+        return ul;
       }
-      return menu_html;
     };
 
+    // Append the menu into the dropdown
     GitLabDropdown.prototype.appendMenu = function(html) {
       var selector;
       selector = '.dropdown-content';
@@ -448,57 +530,71 @@
     };
 
     GitLabDropdown.prototype.renderItem = function(data, group, index) {
-      var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value;
+      var field, fieldName, html, selected, text, url, value;
       if (group == null) {
         group = false;
       }
       if (index == null) {
+        // Render the row
         index = false;
       }
-      html = "";
-      if (data === "divider") {
-        return "<li class='divider'></li>";
-      }
-      if (data === "separator") {
-        return "<li class='separator'></li>";
+      html = document.createElement('li');
+      if (data === 'divider' || data === 'separator') {
+        html.className = data;
+        return html;
       }
+      // Header
       if (data.header != null) {
-        return "<li class='dropdown-header'>" + data.header + "</li>";
+        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;
           }
         }
+        // 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 : '';
         }
-        cssClass = "";
-        if (selected) {
-          cssClass = "is-active";
-        }
         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;
+
+        if (selected) {
+          link.className = 'is-active';
+        }
+
         if (group) {
-          groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
-        } else {
-          groupAttrs = '';
+          link.dataset.group = group;
+          link.dataset.index = index;
         }
-        html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+
+        html.appendChild(link);
       }
       return html;
     };
@@ -520,17 +616,6 @@
       return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
     };
 
-    GitLabDropdown.prototype.highlightRow = function(index) {
-      var selector;
-      if (this.filterInput.val() !== "") {
-        selector = '.dropdown-content li:first-child a';
-        if (this.dropdown.find(".dropdown-toggle-page").length) {
-          selector = ".dropdown-page-one .dropdown-content li:first-child a";
-        }
-        return this.getElement(selector).addClass('is-focused');
-      }
-    };
-
     GitLabDropdown.prototype.rowClicked = function(el) {
       var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
       fieldName = this.options.fieldName;
@@ -545,34 +630,44 @@
           selectedObject = this.renderedData[selectedIndex];
         }
       }
-      value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+
+      if (this.options.vue) {
+        if (el.hasClass(ACTIVE_CLASS)) {
+          el.removeClass(ACTIVE_CLASS);
+        } else {
+          el.addClass(ACTIVE_CLASS);
+        }
+
+        return selectedObject;
+      }
+
+      field = [];
+      value = this.options.id
+        ? this.options.id(selectedObject, el)
+        : selectedObject.id;
       if (isInput) {
         field = $(this.el);
-      } else {
-        field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+      } else if(value) {
+        field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
       }
       if (el.hasClass(ACTIVE_CLASS)) {
         el.removeClass(ACTIVE_CLASS);
-        if (isInput) {
-          field.val('');
-        } else {
-          field.remove();
-        }
-        if (this.options.toggleLabel) {
-          return this.updateLabel(selectedObject, el, this);
-        } else {
-          return selectedObject;
+        if (field && field.length) {
+          if (isInput) {
+            field.val('');
+          } else {
+            field.remove();
+          }
         }
       } else if (el.hasClass(INDETERMINATE_CLASS)) {
         el.addClass(ACTIVE_CLASS);
         el.removeClass(INDETERMINATE_CLASS);
-        if (value == null) {
+        if (field && field.length && value == null) {
           field.remove();
         }
-        if (!field.length && fieldName) {
-          this.addInput(fieldName, value);
+        if ((!field || !field.length) && fieldName) {
+          this.addInput(fieldName, value, selectedObject);
         }
-        return selectedObject;
       } else {
         if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
           this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
@@ -580,26 +675,30 @@
             this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
           }
         }
-        if (value == null) {
+        if (field && field.length && value == null) {
           field.remove();
         }
+        // Toggle active class for the tick mark
         el.addClass(ACTIVE_CLASS);
-        if (this.options.toggleLabel) {
-          this.updateLabel(selectedObject, el, this);
-        }
         if (value != null) {
-          if (!field.length && fieldName) {
-            this.addInput(fieldName, value);
-          } else {
+          if ((!field || !field.length) && fieldName) {
+            this.addInput(fieldName, value, selectedObject);
+          } else if (field && field.length) {
             field.val(value).trigger('change');
           }
         }
-        return selectedObject;
       }
+
+      return selectedObject;
     };
 
-    GitLabDropdown.prototype.addInput = function(fieldName, value) {
+    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);
@@ -609,13 +708,24 @@
 
     GitLabDropdown.prototype.selectRowAtIndex = function(index) {
       var $el, selector;
-      selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
+      // 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) {
-        return $el.first().trigger('click');
+        var href = $el.attr('href');
+        if (href && href !== '#') {
+          Turbolinks.visit(href);
+        } else {
+          $el.first().trigger('click');
+        }
       }
     };
 
@@ -623,7 +733,7 @@
       var $input, ARROW_KEY_CODES, selector;
       ARROW_KEY_CODES = [38, 40];
       $input = this.dropdown.find(".dropdown-input-field");
-      selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
+      selector = SELECTABLE_CLASSES;
       if (this.dropdown.find(".dropdown-toggle-page").length) {
         selector = ".dropdown-page-one " + selector;
       }
@@ -636,11 +746,15 @@
             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;
               }
@@ -651,7 +765,8 @@
             return false;
           }
           if (currentKeyCode === 13 && currentIndex !== -1) {
-            return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
+            e.preventDefault();
+            _this.selectRowAtIndex();
           }
         };
       })(this));
@@ -661,23 +776,40 @@
       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 (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
-        return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
-      } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
-        return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
+      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);
       }
     };
 
diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..f7cbecc0385e9c05779da3a8e7410cbd464b08a5
--- /dev/null
+++ b/app/assets/javascripts/gl_field_error.js.es6
@@ -0,0 +1,164 @@
+/* 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.es6 b/app/assets/javascripts/gl_field_errors.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..6ce392d2a5b54e117f87980da9025971d60ea46e
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -0,0 +1,49 @@
+/* eslint-disable */
+
+//= 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
index 528a673eb15c07a341806fd35d1ca9452a953b52..ce54c34492dbbf984e6e957b6bab0cb42e3456be 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,14 +1,18 @@
+/* eslint-disable */
 (function() {
   this.GLForm = (function() {
     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);
     };
@@ -20,13 +24,16 @@
       if (isNewForm) {
         this.form.find('.div-dropzone').remove();
         this.form.addClass('gfm-form');
-        disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+        // remove notify commit author checkbox for non-commit notes
+        gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
         GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
         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();
       return this.form.show();
     };
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index b95faadc8e72f17e7cdb90eab9203622916bee02..e103748d499d04122991e7bcddfeaa3970c1769b 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,7 +1,12 @@
-
+/* eslint-disable */
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
 /*= require_tree . */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index f041980bc199189fb36599a9f0a366e86b06d898..b796a9abb494d07c5948de5e4e740349006a8ba0 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.StatGraph = (function() {
     function StatGraph() {}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 927d241b35745287305c3aab7988e180894b43e4..818bff0c4131b3b9cb27989d4ba4a14f51ff62d5 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require d3 */
 
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index a646ca1d84f00fb45ee004bd060adc960c9c7be8..dea26a3f1e1b3be112d483f458a4b0e037f02359 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require d3 */
 
@@ -29,8 +30,7 @@
     ContributorsGraph.set_y_domain = function(data) {
       return ContributorsGraph.prototype.y_domain = [
         0, d3.max(data, function(d) {
-          var ref, ref1;
-          return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+          return d.commits = d.commits || d.additions || d.deletions;
         })
       ];
     };
@@ -44,8 +44,7 @@
     ContributorsGraph.init_y_domain = function(data) {
       return ContributorsGraph.prototype.y_domain = [
         0, d3.max(data, function(d) {
-          var ref, ref1;
-          return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+          return d.commits = d.commits || d.additions || d.deletions;
         })
       ];
     };
@@ -147,9 +146,8 @@
       return this.area = d3.svg.area().x(function(d) {
         return x(d.date);
       }).y0(this.height).y1(function(d) {
-        var ref, ref1, xa;
-        xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
-        return y(xa);
+        d.commits = d.commits || d.additions || d.deletions;
+        return y(d.commits);
       }).interpolate("basis");
     };
 
@@ -204,6 +202,7 @@
 
     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 {
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 0d240bed8b61ea2c43d689b3d557ec65d64b7d2a..362a77e868f479bfeb742c880ce544ae8b92f250 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   window.ContributorsStatGraphUtil = {
     parse_log: function(log) {
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index c28ce86d7afc8cc8878b67d5c2265057b3f3421c..774477dc7a99a299e87ca998054be23c67d49908 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.GroupAvatar = (function() {
     function GroupAvatar() {
diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js
deleted file mode 100644
index 4382dd6860f542d75fdb69d94adca8e82397ed04..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/groups.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(function() {
-  this.GroupMembers = (function() {
-    function GroupMembers() {
-      $('li.group_member').bind('ajax:success', function() {
-        return $(this).fadeOut();
-      });
-    }
-
-    return GroupMembers;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index fd5b6dc0ddd405ff5ee3a9076ec9cb5b95de803f..e3c39c895bad004f2a3408470848b9ee99cc20ed 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var slice = [].slice;
 
@@ -5,14 +6,16 @@
     function GroupsSelect() {
       $('.ajax-groups-select').each((function(_this) {
         return function(i, select) {
-          var skip_ldap;
-          skip_ldap = $(select).hasClass('skip_ldap');
+          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) {
-              return Api.groups(query.term, skip_ldap, function(groups) {
+              options = { all_available: all_available, skip_groups: skip_groups };
+              return Api.groups(query.term, options, function(groups) {
                 var data;
                 data = {
                   results: groups
@@ -38,6 +41,7 @@
               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;
             }
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
new file mode 100644
index 0000000000000000000000000000000000000000..81fcaf0643016ceb3bbe75cda1d8d11e75f2963d
--- /dev/null
+++ b/app/assets/javascripts/header.js
@@ -0,0 +1,10 @@
+/* eslint-disable */
+(function() {
+
+  $(document).on('todo:toggle', function(e, count) {
+    var $todoPendingCount = $('.todos-pending-count');
+    $todoPendingCount.text(gl.text.addDelimiter(count));
+    $todoPendingCount.toggleClass('hidden', count === 0);
+  });
+
+})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 0f840821f5394149151f69019c6675f50b9c94ed..c53f7c88aa2607d30386aaf32429f2a45a893a8a 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ImporterStatus = (function() {
     function ImporterStatus(jobs_url, import_url) {
@@ -10,21 +11,24 @@
     ImporterStatus.prototype.initStatusPage = function() {
       $('.js-add-to-import').off('click').on('click', (function(_this) {
         return function(e) {
-          var $btn, $namespace_input, $target_field, $tr, id, new_namespace;
+          var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
           $btn = $(e.currentTarget);
           $tr = $btn.closest('tr');
           $target_field = $tr.find('.import-target');
-          $namespace_input = $target_field.find('input');
+          $namespace_input = $target_field.find('.js-select-namespace option:selected');
           id = $tr.attr('id').replace('repo_', '');
-          new_namespace = null;
+          target_namespace = null;
+          newName = null;
           if ($namespace_input.length > 0) {
-            new_namespace = $namespace_input.prop('value');
-            $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name')));
+            target_namespace = $namespace_input[0].innerHTML;
+            newName = $target_field.find('#path').prop('value');
+            $target_field.empty().append(target_namespace + "/" + newName);
           }
           $btn.disable().addClass('is-loading');
           return $.post(_this.import_url, {
             repo_id: id,
-            new_namespace: new_namespace
+            target_namespace: target_namespace,
+            new_name: newName
           }, {
             dataType: 'script'
           });
@@ -70,7 +74,7 @@
     if ($('.js-importer-status').length) {
       var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
       var importPath = $('.js-importer-status').data('import-path');
-      
+
       new ImporterStatus(jobsImportPath, importPath);
     }
   });
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js
deleted file mode 100644
index d0305c6c6a1194e60452e393967c890c9825a0b2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable.js
+++ /dev/null
@@ -1,86 +0,0 @@
-(function() {
-  var issuable_created;
-
-  issuable_created = false;
-
-  this.Issuable = {
-    init: function() {
-      Issuable.initTemplates();
-      Issuable.initSearch();
-      Issuable.initChecks();
-      return Issuable.initLabelFilterRemove();
-    },
-    initTemplates: function() {
-      return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
-    },
-    initSearch: function() {
-      this.timer = null;
-      return $('#issue_search').off('keyup').on('keyup', function() {
-        clearTimeout(this.timer);
-        return this.timer = setTimeout(function() {
-          var $form, $input, $search;
-          $search = $('#issue_search');
-          $form = $('.js-filter-form');
-          $input = $("input[name='" + ($search.attr('name')) + "']", $form);
-          if ($input.length === 0) {
-            $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
-          } else {
-            $input.val($search.val());
-          }
-          if ($search.val() !== '') {
-            return Issuable.filterResults($form);
-          }
-        }, 500);
-      });
-    },
-    initLabelFilterRemove: function() {
-      return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
-        var $button;
-        $button = $(this);
-        $('input[name="label_name[]"]').filter(function() {
-          return this.value === $button.data('label');
-        }).remove();
-        Issuable.filterResults($('.filter-form'));
-        return $('.js-label-select').trigger('update.label');
-      });
-    },
-    filterResults: (function(_this) {
-      return function(form) {
-        var formAction, formData, issuesUrl;
-        formData = form.serialize();
-        formAction = form.attr('action');
-        issuesUrl = formAction;
-        issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
-        issuesUrl += formData;
-        return Turbolinks.visit(issuesUrl);
-      };
-    })(this),
-    initChecks: function() {
-      this.issuableBulkActions = $('.bulk-update').data('bulkActions');
-      $('.check_all_issues').off('click').on('click', function() {
-        $('.selected_issue').prop('checked', this.checked);
-        return Issuable.checkChanged();
-      });
-      return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
-    },
-    checkChanged: function() {
-      var checked_issues, ids;
-      checked_issues = $('.selected_issue:checked');
-      if (checked_issues.length > 0) {
-        ids = $.map(checked_issues, function(value) {
-          return $(value).data('id');
-        });
-        $('#update_issues_ids').val(ids);
-        $('.issues-other-filters').hide();
-        $('.issues_bulk_update').show();
-      } else {
-        $('#update_issues_ids').val([]);
-        $('.issues_bulk_update').hide();
-        $('.issues-other-filters').show();
-        this.issuableBulkActions.willUpdateLabels = false;
-      }
-      return true;
-    }
-  };
-
-}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..46503c290aed9ca9b6e9843602905a181f5a4f63
--- /dev/null
+++ b/app/assets/javascripts/issuable.js.es6
@@ -0,0 +1,182 @@
+/* eslint-disable */
+(function() {
+  var issuable_created;
+
+  issuable_created = false;
+
+  this.Issuable = {
+    init: function() {
+      Issuable.initTemplates();
+      Issuable.initSearch();
+      Issuable.initChecks();
+      Issuable.initResetFilters();
+      Issuable.resetIncomingEmailToken();
+      return Issuable.initLabelFilterRemove();
+    },
+    initTemplates: function() {
+      return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
+    },
+    initSearch: function() {
+      const $searchInput = $('#issuable_search');
+
+      Issuable.initSearchState($searchInput);
+
+      // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+      const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
+
+      $searchInput.off('keyup').on('keyup', debouncedExecSearch);
+
+      // ensures existing filters are preserved when manually submitted
+      $('#issuable_search_form').on('submit', (e) => {
+        e.preventDefault();
+        debouncedExecSearch(e);
+      });
+
+    },
+    initSearchState: function($searchInput) {
+      const currentSearchVal = $searchInput.val();
+
+      Issuable.searchState = {
+        elem: $searchInput,
+        current: currentSearchVal
+      };
+
+      Issuable.maybeFocusOnSearch();
+    },
+    accessSearchPristine: function(set) {
+      // store reference to previous value to prevent search on non-mutating keyup
+      const state = Issuable.searchState;
+      const currentSearchVal = state.elem.val();
+
+      if (set) {
+        state.current = currentSearchVal;
+      } else {
+        return state.current === currentSearchVal;
+      }
+    },
+    maybeFocusOnSearch: function() {
+      const currentSearchVal = Issuable.searchState.current;
+      if (currentSearchVal && currentSearchVal !== '') {
+        const queryLength = currentSearchVal.length;
+        const $searchInput = Issuable.searchState.elem;
+
+      /* The following ensures that the cursor is initially placed at
+        * the end of search input when focus is applied. It accounts
+        * for differences in browser implementations of `setSelectionRange`
+        * and cursor placement for elements in focus.
+      */
+        $searchInput.focus();
+        if ($searchInput.setSelectionRange) {
+          $searchInput.setSelectionRange(queryLength, queryLength);
+        } else {
+          $searchInput.val(currentSearchVal);
+        }
+      }
+    },
+    executeSearch: function(e) {
+      const $search = $('#issuable_search');
+      const $searchName = $search.attr('name');
+      const $searchValue = $search.val();
+      const $filtersForm = $('.js-filter-form');
+      const $input = $(`input[name='${$searchName}']`, $filtersForm);
+      const isPristine = Issuable.accessSearchPristine();
+
+      if (isPristine) {
+        return;
+      }
+
+      if (!$input.length) {
+        $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
+      } else {
+        $input.val($searchValue);
+      }
+
+      Issuable.filterResults($filtersForm);
+    },
+    initLabelFilterRemove: function() {
+      return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
+        var $button;
+        $button = $(this);
+        // Remove the label input box
+        $('input[name="label_name[]"]').filter(function() {
+          return this.value === $button.data('label');
+        }).remove();
+        // Submit the form to get new data
+        Issuable.filterResults($('.filter-form'));
+      });
+    },
+    filterResults: (function(_this) {
+      return function(form) {
+        var formAction, formData, issuesUrl;
+        formData = form.serialize();
+        formAction = form.attr('action');
+        issuesUrl = formAction;
+        issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
+        issuesUrl += formData;
+        return Turbolinks.visit(issuesUrl);
+      };
+    })(this),
+    initResetFilters: function() {
+      $('.reset-filters').on('click', function(e) {
+        e.preventDefault();
+        const target = e.target;
+        const $form = $(target).parents('.js-filter-form');
+        const baseIssuesUrl = target.href;
+
+        $form.attr('action', baseIssuesUrl);
+        Turbolinks.visit(baseIssuesUrl);
+      });
+    },
+    initChecks: function() {
+      this.issuableBulkActions = $('.bulk-update').data('bulkActions');
+      $('.check_all_issues').off('click').on('click', function() {
+        $('.selected_issue').prop('checked', this.checked);
+        return Issuable.checkChanged();
+      });
+      return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
+    },
+    checkChanged: function() {
+      const $checkedIssues = $('.selected_issue:checked');
+      const $updateIssuesIds = $('#update_issuable_ids');
+      const $issuesOtherFilters = $('.issues-other-filters');
+      const $issuesBulkUpdate = $('.issues_bulk_update');
+
+      if ($checkedIssues.length > 0) {
+        let ids = $.map($checkedIssues, function(value) {
+          return $(value).data('id');
+        });
+        $updateIssuesIds.val(ids);
+        $issuesOtherFilters.hide();
+        $issuesBulkUpdate.show();
+      } else {
+        $updateIssuesIds.val([]);
+        $issuesBulkUpdate.hide();
+        $issuesOtherFilters.show();
+        this.issuableBulkActions.willUpdateLabels = false;
+      }
+      return true;
+    },
+
+    resetIncomingEmailToken: function() {
+      $('.incoming-email-token-reset').on('click', function(e) {
+        e.preventDefault();
+
+        $.ajax({
+          type: 'PUT',
+          url: $('.incoming-email-token-reset').attr('href'),
+          dataType: 'json',
+          success: function(response) {
+            $('#issue_email').val(response.new_issue_address).focus();
+          },
+          beforeSend: function() {
+            $('.incoming-email-token-reset').text('resetting...');
+          },
+          complete: function() {
+            $('.incoming-email-token-reset').text('reset it');
+          }
+        });
+      });
+    }
+  };
+
+}).call(this);
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 8147e83ffe8f0f343e455e0395409e50ac67b2d1..fae49ee614413532b61f15ca762eb1794054808d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.IssuableContext = (function() {
     function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 297d4f029f0e50c541b8474f850c74f94ef271fb..849b45756ee553455d91ecc6ee4b6b410dcf5809 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -44,8 +45,8 @@
     };
 
     IssuableForm.prototype.handleSubmit = function() {
-      var ref, ref1;
-      if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) {
+      var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
+      if ((parseInt(fieldId) || 0) > 0) {
         if (!confirm(this.issueMoveConfirmMsg)) {
           return false;
         }
@@ -102,20 +103,34 @@
     };
 
     IssuableForm.prototype.initMoveDropdown = function() {
-      var $moveDropdown;
+      var $moveDropdown, pageSize;
       $moveDropdown = $('.js-move-dropdown');
       if ($moveDropdown.length) {
+        pageSize = $moveDropdown.data('page-size');
         return $('.js-move-dropdown').select2({
           ajax: {
             url: $moveDropdown.data('projects-url'),
-            results: function(data) {
+            quietMillis: 125,
+            data: function(term, page, context) {
               return {
-                results: data
+                search: term,
+                offset_id: context
               };
             },
-            data: function(query) {
+            results: function(data) {
+              var context,
+                more;
+
+              if (data.length >= pageSize)
+                more = true;
+
+              if (data[data.length - 1])
+                context = data[data.length - 1].id;
+
               return {
-                search: query
+                results: data,
+                more: more,
+                context: context
               };
             }
           },
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 6838d9d8da15953f74ab2c2df0bf878c1fa1cebf..67ace697936b1af2771e7c7e94468afcf1df939b 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,10 +1,7 @@
+/* eslint-disable */
 
 /*= require flash */
-
-
 /*= require jquery.waitforimages */
-
-
 /*= require task_list */
 
 (function() {
@@ -13,6 +10,7 @@
   this.Issue = (function() {
     function Issue() {
       this.submitNoteForm = bind(this.submitNoteForm, this);
+      // Prevent duplicate event bindings
       this.disableTaskList();
       if ($('a.btn-close').length) {
         this.initTaskList();
@@ -97,8 +95,14 @@
       return $.ajax({
         type: 'PATCH',
         url: $('form.js-issuable-update').attr('action'),
-        data: patchData
+        data: patchData,
+        success: function(issue) {
+          document.querySelector('#task_status').innerText = issue.task_status;
+          document.querySelector('#task_status_short').innerText = issue.task_status_short;
+        }
       });
+    // TODO (rspeicher): Make the issue description inline-editable like a note so
+    // that we can re-use its form here
     };
 
     Issue.prototype.initMergeRequests = function() {
@@ -127,7 +131,9 @@
 
     Issue.prototype.initCanCreateBranch = function() {
       var $container;
-      $container = $('div#new-branch');
+      $container = $('#new-branch');
+      // If the user doesn't have the required permissions the container isn't
+      // rendered at all.
       if ($container.length === 0) {
         return;
       }
@@ -139,7 +145,6 @@
         if (data.can_create_branch) {
           $container.find('.checking').hide();
           $container.find('.available').show();
-          return $container.find('a').attr('disabled', false);
         } else {
           $container.find('.checking').hide();
           return $container.find('.unavailable').show();
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 076e39729444f1deb81f96cb47d881a46c6832f6..d7262e5eb741bb6976531cca87c8d54d49369097 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.IssueStatusSelect = (function() {
     function IssueStatusSelect() {
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js
deleted file mode 100644
index 98d3358ba921da1fd57dae38f22a1cddd6e65143..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issues-bulk-assignment.js
+++ /dev/null
@@ -1,161 +0,0 @@
-(function() {
-  this.IssuableBulkActions = (function() {
-    function IssuableBulkActions(opts) {
-      var ref, ref1, ref2;
-      if (opts == null) {
-        opts = {};
-      }
-      this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue');
-      this.form.data('bulkActions', this);
-      this.willUpdateLabels = false;
-      this.bindEvents();
-      Issuable.initChecks();
-    }
-
-    IssuableBulkActions.prototype.getElement = function(selector) {
-      return this.container.find(selector);
-    };
-
-    IssuableBulkActions.prototype.bindEvents = function() {
-      return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
-    };
-
-    IssuableBulkActions.prototype.onFormSubmit = function(e) {
-      e.preventDefault();
-      return this.submit();
-    };
-
-    IssuableBulkActions.prototype.submit = function() {
-      var _this, xhr;
-      _this = this;
-      xhr = $.ajax({
-        url: this.form.attr('action'),
-        method: this.form.attr('method'),
-        dataType: 'JSON',
-        data: this.getFormDataAsObject()
-      });
-      xhr.done(function(response, status, xhr) {
-        return location.reload();
-      });
-      xhr.fail(function() {
-        return new Flash("Issue update failed");
-      });
-      return xhr.always(this.onFormSubmitAlways.bind(this));
-    };
-
-    IssuableBulkActions.prototype.onFormSubmitAlways = function() {
-      return this.form.find('[type="submit"]').enable();
-    };
-
-    IssuableBulkActions.prototype.getSelectedIssues = function() {
-      return this.issues.has('.selected_issue:checked');
-    };
-
-    IssuableBulkActions.prototype.getLabelsFromSelection = function() {
-      var labels;
-      labels = [];
-      this.getSelectedIssues().map(function() {
-        var _labels;
-        _labels = $(this).data('labels');
-        if (_labels) {
-          return _labels.map(function(labelId) {
-            if (labels.indexOf(labelId) === -1) {
-              return labels.push(labelId);
-            }
-          });
-        }
-      });
-      return labels;
-    };
-
-
-    /**
-     * Will return only labels that were marked previously and the user has unmarked
-     * @return {Array} Label IDs
-     */
-
-    IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
-      var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
-      result = [];
-      labelsToKeep = [];
-      ref = this.getElement('.labels-filter .is-indeterminate');
-      for (i = 0, len = ref.length; i < len; i++) {
-        el = ref[i];
-        labelsToKeep.push($(el).data('labelId'));
-      }
-      ref1 = this.getLabelsFromSelection();
-      for (j = 0, len1 = ref1.length; j < len1; j++) {
-        id = ref1[j];
-        if (labelsToKeep.indexOf(id) === -1) {
-          result.push(id);
-        }
-      }
-      return result;
-    };
-
-
-    /**
-     * Simple form serialization, it will return just what we need
-     * Returns key/value pairs from form data
-     */
-
-    IssuableBulkActions.prototype.getFormDataAsObject = function() {
-      var formData;
-      formData = {
-        update: {
-          state_event: this.form.find('input[name="update[state_event]"]').val(),
-          assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
-          milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
-          issues_ids: this.form.find('input[name="update[issues_ids]"]').val(),
-          subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
-          add_label_ids: [],
-          remove_label_ids: []
-        }
-      };
-      if (this.willUpdateLabels) {
-        this.getLabelsToApply().map(function(id) {
-          return formData.update.add_label_ids.push(id);
-        });
-        this.getLabelsToRemove().map(function(id) {
-          return formData.update.remove_label_ids.push(id);
-        });
-      }
-      return formData;
-    };
-
-    IssuableBulkActions.prototype.getLabelsToApply = function() {
-      var $labels, labelIds;
-      labelIds = [];
-      $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
-      $labels.each(function(k, label) {
-        if (label) {
-          return labelIds.push(parseInt($(label).val()));
-        }
-      });
-      return labelIds;
-    };
-
-
-    /**
-     * Returns Label IDs that will be removed from issue selection
-     * @return {Array} Array of labels IDs
-     */
-
-    IssuableBulkActions.prototype.getLabelsToRemove = function() {
-      var indeterminatedLabels, labelsToApply, result;
-      result = [];
-      indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
-      labelsToApply = this.getLabelsToApply();
-      indeterminatedLabels.map(function(id) {
-        if (labelsToApply.indexOf(id) === -1) {
-          return result.push(id);
-        }
-      });
-      return result;
-    };
-
-    return IssuableBulkActions;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9697fb33566b3a534c9950a79a8764e4811287cc
--- /dev/null
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -0,0 +1,150 @@
+/* eslint-disable */
+((global) => {
+
+  class IssuableBulkActions {
+    constructor({ container, form, issues } = {}) {
+      this.container = container || $('.content'),
+      this.form = form || this.getElement('.bulk-update');
+      this.issues = issues || this.getElement('.issues-list .issue');
+      this.form.data('bulkActions', this);
+      this.willUpdateLabels = false;
+      this.bindEvents();
+      // Fixes bulk-assign not working when navigating through pages
+      Issuable.initChecks();
+    }
+
+    getElement(selector) {
+      return this.container.find(selector);
+    }
+
+    bindEvents() {
+      return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
+    }
+
+    onFormSubmit(e) {
+      e.preventDefault();
+      return this.submit();
+    }
+
+    submit() {
+      const _this = this;
+      const xhr = $.ajax({
+        url: this.form.attr('action'),
+        method: this.form.attr('method'),
+        dataType: 'JSON',
+        data: this.getFormDataAsObject()
+      });
+      xhr.done(() => window.location.reload());
+      xhr.fail(() => new Flash("Issue update failed"));
+      return xhr.always(this.onFormSubmitAlways.bind(this));
+    }
+
+    onFormSubmitAlways() {
+      return this.form.find('[type="submit"]').enable();
+    }
+
+    getSelectedIssues() {
+      return this.issues.has('.selected_issue:checked');
+    }
+
+    getLabelsFromSelection() {
+      const labels = [];
+      this.getSelectedIssues().map(function() {
+        const labelsData = $(this).data('labels');
+        if (labelsData) {
+          return labelsData.map(function(labelId) {
+            if (labels.indexOf(labelId) === -1) {
+              return labels.push(labelId);
+            }
+          });
+        }
+      });
+      return labels;
+    }
+
+
+    /**
+     * Will return only labels that were marked previously and the user has unmarked
+     * @return {Array} Label IDs
+     */
+
+    getUnmarkedIndeterminedLabels() {
+      const result = [];
+      const labelsToKeep = [];
+
+      this.getElement('.labels-filter .is-indeterminate')
+        .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+
+      this.getLabelsFromSelection().forEach((id) => {
+        if (labelsToKeep.indexOf(id) === -1) {
+          result.push(id);
+        }
+      });
+
+      return result;
+    }
+
+
+    /**
+     * Simple form serialization, it will return just what we need
+     * Returns key/value pairs from form data
+     */
+
+    getFormDataAsObject() {
+      const formData = {
+        update: {
+          state_event: this.form.find('input[name="update[state_event]"]').val(),
+          assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
+          milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
+          issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
+          subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
+          add_label_ids: [],
+          remove_label_ids: []
+        }
+      };
+      if (this.willUpdateLabels) {
+        this.getLabelsToApply().map(function(id) {
+          return formData.update.add_label_ids.push(id);
+        });
+        this.getLabelsToRemove().map(function(id) {
+          return formData.update.remove_label_ids.push(id);
+        });
+      }
+      return formData;
+    }
+
+    getLabelsToApply() {
+      const labelIds = [];
+      const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
+      $labels.each(function(k, label) {
+        if (label) {
+          return labelIds.push(parseInt($(label).val()));
+        }
+      });
+      return labelIds;
+    }
+
+
+    /**
+     * Returns Label IDs that will be removed from issue selection
+     * @return {Array} Array of labels IDs
+     */
+
+    getLabelsToRemove() {
+      const result = [];
+      const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
+      const labelsToApply = this.getLabelsToApply();
+      indeterminatedLabels.map(function(id) {
+        // We need to exclude label IDs that will be applied
+        // By not doing this will cause issues from selection to not add labels at all
+        if (labelsToApply.indexOf(id) === -1) {
+          return result.push(id);
+        }
+      });
+      return result;
+    }
+  }
+
+  global.IssuableBulkActions = IssuableBulkActions;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..175623e74483b0c1d4e710e4fba6642254575b1a
--- /dev/null
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -0,0 +1,107 @@
+/* eslint-disable */
+((global) => {
+
+  class LabelManager {
+    constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
+      this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
+      this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
+      this.otherLabels = otherLabels || $('.js-other-labels');
+      this.errorMessage = 'Unable to update label prioritization at this time';
+      this.prioritizedLabels.sortable({
+        items: 'li',
+        placeholder: 'list-placeholder',
+        axis: 'y',
+        update: this.onPrioritySortUpdate.bind(this)
+      });
+      this.bindEvents();
+    }
+
+    bindEvents() {
+      return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+    }
+
+    onTogglePriorityClick(e) {
+      e.preventDefault();
+      const _this = e.data;
+      const $btn = $(e.currentTarget);
+      const $label = $(`#${$btn.data('domId')}`);
+      const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+      const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
+      $tooltip.tooltip('destroy');
+      return _this.toggleLabelPriority($label, action);
+    }
+
+    toggleLabelPriority($label, action, persistState) {
+      if (persistState == null) {
+        persistState = true;
+      }
+      let xhr;
+      const _this = this;
+      const url = $label.find('.js-toggle-priority').data('url');
+      let $target = this.prioritizedLabels;
+      let $from = this.otherLabels;
+      if (action === 'remove') {
+        $target = this.otherLabels;
+        $from = this.prioritizedLabels;
+      }
+      if ($from.find('li').length === 1) {
+        $from.find('.empty-message').removeClass('hidden');
+      }
+      if (!$target.find('li').length) {
+        $target.find('.empty-message').addClass('hidden');
+      }
+      $label.detach().appendTo($target);
+      // Return if we are not persisting state
+      if (!persistState) {
+        return;
+      }
+      if (action === 'remove') {
+        xhr = $.ajax({
+          url,
+          type: 'DELETE'
+        });
+        // Restore empty message
+        if (!$from.find('li').length) {
+          $from.find('.empty-message').removeClass('hidden');
+        }
+      } else {
+        xhr = this.savePrioritySort($label, action);
+      }
+      return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+    }
+
+    onPrioritySortUpdate() {
+      const xhr = this.savePrioritySort();
+      return xhr.fail(function() {
+        return new Flash(this.errorMessage, 'alert');
+      });
+    }
+
+    savePrioritySort() {
+      return $.post({
+        url: this.prioritizedLabels.data('url'),
+        data: {
+          label_ids: this.getSortedLabelsIds()
+        }
+      });
+    }
+
+    rollbackLabelPosition($label, originalAction) {
+      const action = originalAction === 'remove' ? 'add' : 'remove';
+      this.toggleLabelPriority($label, action, false);
+      return new Flash(this.errorMessage, 'alert');
+    }
+
+    getSortedLabelsIds() {
+      const sortedIds = [];
+      this.prioritizedLabels.find('li').each(function() {
+        sortedIds.push($(this).data('id'));
+      });
+      return sortedIds;
+    }
+  }
+
+  gl.LabelManager = LabelManager;
+
+})(window.gl || (window.gl = {}));
+
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index fe071fca67ca4396c312976be004e4e87cd875a5..3033e8ca5c2bccb783949bdaf8a0f02e81b6286a 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -26,13 +27,16 @@
       var previewColor;
       previewColor = $('input#label_color').val();
       return $('div.label-color-preview').css('background-color', previewColor);
+    // Updates the the preview color with the hex-color input
     };
 
+    // Updates the preview color with a click on a suggested color
     Labels.prototype.setSuggestedColor = function(e) {
       var color;
       color = $(e.currentTarget).data('color');
       $('input#label_color').val(color);
       this.updateColorPreview();
+      // Notify the form, that color has changed
       $('.label-form').trigger('keyup');
       return e.preventDefault();
     };
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 1bb0b67d0e86519a07bbec16bf33faaa750417b3..c334e3e0c0265c1745c305da329c9c435d5fd37a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,34 +1,41 @@
+/* eslint-disable */
 (function() {
   this.LabelsSelect = (function() {
     function LabelsSelect() {
       var _this;
       _this = this;
       $('.js-label-select').each(function(i, dropdown) {
-        var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo;
+        var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
         $dropdown = $(dropdown);
-        projectId = $dropdown.data('project-id');
+        $toggleText = $dropdown.find('.dropdown-toggle-text');
+        namespacePath = $dropdown.data('namespace-path');
+        projectPath = $dropdown.data('project-path');
         labelUrl = $dropdown.data('labels');
         issueUpdateURL = $dropdown.data('issueUpdate');
         selectedLabel = $dropdown.data('selected');
         if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
           selectedLabel = selectedLabel.split(',');
         }
-        newLabelField = $('#new_label_name');
-        newColorField = $('#new_label_color');
         showNo = $dropdown.data('show-no');
         showAny = $dropdown.data('show-any');
+        showMenuAbove = $dropdown.data('showMenuAbove');
         defaultLabel = $dropdown.data('default-label');
         abilityName = $dropdown.data('ability-name');
         $selectbox = $dropdown.closest('.selectbox');
         $block = $selectbox.closest('.block');
-        $form = $dropdown.closest('form');
+        $form = $dropdown.closest('form, .js-issuable-update');
         $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+        $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
         $value = $block.find('.value');
-        $newLabelError = $('.js-label-error');
-        $colorPreview = $('.js-dropdown-label-color-preview');
-        $newLabelCreateButton = $('.js-new-label-btn');
-        $newLabelError.hide();
         $loading = $block.find('.block-loading').fadeOut();
+        fieldName = $dropdown.data('field-name');
+        useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+        propertyName = useId ? 'id' : 'title';
+        initialSelected = $selectbox
+          .find('input[name="' + $dropdown.data('field-name') + '"]')
+          .map(function () {
+            return this.value;
+          }).get();
         if (issueUpdateURL != null) {
           issueURLSplit = issueUpdateURL.split('/');
         }
@@ -36,67 +43,22 @@
           labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
           labelNoneHTMLTemplate = '<span class="no-value">None</span>';
         }
-        if (newLabelField.length) {
-          $('.suggest-colors-dropdown a').on("click", function(e) {
-            e.preventDefault();
-            e.stopPropagation();
-            newColorField.val($(this).data('color')).trigger('change');
-            return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active');
-          });
-          resetForm = function() {
-            newLabelField.val('').trigger('change');
-            newColorField.val('').trigger('change');
-            return $colorPreview.css('background-color', '').parent().removeClass('is-active');
-          };
-          $('.dropdown-menu-back').on('click', function() {
-            return resetForm();
-          });
-          $('.js-cancel-label-btn').on('click', function(e) {
-            e.preventDefault();
-            e.stopPropagation();
-            resetForm();
-            return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
-          });
-          enableLabelCreateButton = function() {
-            if (newLabelField.val() !== '' && newColorField.val() !== '') {
-              $newLabelError.hide();
-              return $newLabelCreateButton.enable();
-            } else {
-              return $newLabelCreateButton.disable();
-            }
-          };
-          saveLabel = function() {
-            return Api.newLabel(projectId, {
-              name: newLabelField.val(),
-              color: newColorField.val()
-            }, function(label) {
-              $newLabelCreateButton.enable();
-              if (label.message != null) {
-                var errorText = label.message;
-                if (_.isObject(label.message)) {
-                  errorText = _.map(label.message, function(value, key) {
-                    return key + " " + value[0];
-                  }).join('<br/>');
-                }
-                return $newLabelError.html(errorText).show();
-              } else {
-                return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
-              }
-            });
-          };
-          newLabelField.on('keyup change', enableLabelCreateButton);
-          newColorField.on('keyup change', enableLabelCreateButton);
-          $newLabelCreateButton.disable().on('click', function(e) {
-            e.preventDefault();
-            e.stopPropagation();
-            return saveLabel();
-          });
+
+        $sidebarLabelTooltip.tooltip();
+
+        if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+          new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
         }
+
         saveLabelData = function() {
           var data, selected;
-          selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
+          selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
             return this.value;
           }).get();
+
+          if (_.isEqual(initialSelected, selected)) return;
+          initialSelected = selected;
+
           data = {};
           data[abilityName] = {};
           data[abilityName].label_ids = selected;
@@ -111,7 +73,7 @@
             dataType: 'JSON',
             data: data
           }).done(function(data) {
-            var labelCount, template;
+            var labelCount, template, labelTooltipTitle, labelTitles;
             $loading.fadeOut();
             $dropdown.trigger('loaded.gl.dropdown');
             $selectbox.hide();
@@ -120,11 +82,34 @@
             if (data.labels.length) {
               template = labelHTMLTemplate(data);
               labelCount = data.labels.length;
-            } else {
+            }
+            else {
               template = labelNoneHTMLTemplate;
             }
             $value.removeAttr('style').html(template);
             $sidebarCollapsedValue.text(labelCount);
+
+            if (data.labels.length) {
+              labelTitles = data.labels.map(function(label) {
+                return label.title;
+              });
+
+              if (labelTitles.length > 5) {
+                labelTitles = labelTitles.slice(0, 5);
+                labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+              }
+
+              labelTooltipTitle = labelTitles.join(', ');
+            }
+            else {
+              labelTooltipTitle = '';
+              $sidebarLabelTooltip.tooltip('destroy');
+            }
+
+            $sidebarLabelTooltip
+              .attr('title', labelTooltipTitle)
+              .tooltip('fixTitle');
+
             $('.has-tooltip', $value).tooltip({
               container: 'body'
             });
@@ -138,6 +123,7 @@
           });
         };
         return $dropdown.glDropdown({
+          showMenuAbove: showMenuAbove,
           data: function(term, callback) {
             return $.ajax({
               url: labelUrl
@@ -157,23 +143,29 @@
                 };
               }).value();
               if ($dropdown.hasClass('js-extra-options')) {
+                var extraData = [];
                 if (showNo) {
-                  data.unshift({
+                  extraData.unshift({
                     id: 0,
                     title: 'No Label'
                   });
                 }
                 if (showAny) {
-                  data.unshift({
+                  extraData.unshift({
                     isAny: true,
                     title: 'Any Label'
                   });
                 }
-                if (data.length > 2) {
-                  data.splice(2, 0, 'divider');
+                if (extraData.length) {
+                  extraData.push('divider');
+                  data = extraData.concat(data);
                 }
               }
-              return callback(data);
+
+              callback(data);
+              if (showMenuAbove) {
+                $dropdown.data('glDropdown').positionMenuAbove();
+              }
             });
           },
           renderRow: function(label, instance) {
@@ -181,7 +173,7 @@
             $li = $('<li>');
             $a = $('<a href="#">');
             selectedClass = [];
-            removesAll = label.id === 0 || (label.id == null);
+            removesAll = label.id <= 0 || (label.id == null);
             if ($dropdown.hasClass('js-filter-bulk-update')) {
               indeterminate = instance.indeterminateIds;
               active = instance.activeIds;
@@ -189,15 +181,17 @@
                 selectedClass.push('is-indeterminate');
               }
               if (active.indexOf(label.id) !== -1) {
+                // Remove is-indeterminate class if the item will be marked as active
                 i = selectedClass.indexOf('is-indeterminate');
                 if (i !== -1) {
                   selectedClass.splice(i, 1);
                 }
                 selectedClass.push('is-active');
+                // Add input manually
                 instance.addInput(this.fieldName, label.id);
               }
             }
-            if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) {
+            if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
               selectedClass.push('is-active');
             }
             if ($dropdown.hasClass('js-multiselect') && removesAll) {
@@ -205,6 +199,7 @@
             }
             if (label.duplicate) {
               spacing = 100 / label.color.length;
+              // Reduce the colors to 4
               label.color = label.color.filter(function(color, i) {
                 return i < 4;
               });
@@ -215,21 +210,25 @@
                 return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
               }).join(',');
               color = "linear-gradient(" + color + ")";
-            } else {
+            }
+            else {
               if (label.color != null) {
                 color = label.color[0];
               }
             }
             if (color) {
               colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
-            } else {
+            }
+            else {
               colorEl = '';
             }
+            // We need to identify which items are actually labels
             if (label.id) {
               selectedClass.push('label-item');
               $a.attr('data-label-id', label.id);
             }
             $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+            // Return generated html
             return $li.html($a).prop('outerHTML');
           },
           persistWhenHide: $dropdown.data('persistWhenHide'),
@@ -238,30 +237,46 @@
           },
           selectable: true,
           filterable: true,
+          selected: $dropdown.data('selected') || [],
           toggleLabel: function(selected, el) {
-            var selected_labels;
-            selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
-            if (selected && (selected.title != null)) {
-              if (selected_labels.length > 1) {
-                return selected.title + " +" + (selected_labels.length - 1) + " more";
-              } else {
-                return selected.title;
-              }
-            } else if (!selected && selected_labels.length !== 0) {
-              if (selected_labels.length > 1) {
-                return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
-              } else if (selected_labels.length === 1) {
-                return $(selected_labels).text();
-              }
-            } else {
+            var isSelected = el !== null ? el.hasClass('is-active') : false;
+            var title = selected.title;
+            var selectedLabels = this.selected;
+
+            if (selected.id === 0) {
+              this.selected = [];
+              return 'No Label';
+            }
+            else if (isSelected) {
+              this.selected.push(title);
+            }
+            else {
+              var index = this.selected.indexOf(title);
+              this.selected.splice(index, 1);
+            }
+
+            if (selectedLabels.length === 1) {
+              return selectedLabels;
+            }
+            else if (selectedLabels.length) {
+              return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+            }
+            else {
               return defaultLabel;
             }
           },
           fieldName: $dropdown.data('field-name'),
           id: function(label) {
+            if (label.id <= 0) return label.title;
+
+            if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+              return label.id;
+            }
+
             if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
               return label.title;
-            } else {
+            }
+            else {
               return label.id;
             }
           },
@@ -271,46 +286,113 @@
             isIssueIndex = page === 'projects:issues:index';
             isMRIndex = page === 'projects:merge_requests:index';
             $selectbox.hide();
+            // display:block overrides the hide-collapse rule
             $value.removeAttr('style');
+
+            if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+              return;
+            }
+
+            if ($('html').hasClass('issue-boards-page')) {
+              return;
+            }
             if ($dropdown.hasClass('js-multiselect')) {
               if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
                 selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
                 Issuable.filterResults($dropdown.closest('form'));
-              } else if ($dropdown.hasClass('js-filter-submit')) {
+              }
+              else if ($dropdown.hasClass('js-filter-submit')) {
                 $dropdown.closest('form').submit();
-              } else {
+              }
+              else {
                 if (!$dropdown.hasClass('js-filter-bulk-update')) {
                   saveLabelData();
                 }
               }
             }
             if ($dropdown.hasClass('js-filter-bulk-update')) {
+              // If we are persisting state we need the classes
               if (!this.options.persistWhenHide) {
                 return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
               }
             }
           },
           multiSelect: $dropdown.hasClass('js-multiselect'),
-          clicked: function(label) {
+          vue: $dropdown.hasClass('js-issue-board-sidebar'),
+          clicked: function(label, $el, e) {
             var isIssueIndex, isMRIndex, page;
             _this.enableBulkLabelDropdown();
-            if ($dropdown.hasClass('js-filter-bulk-update')) {
+
+            if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+              $dropdown.parent()
+                .find('.dropdown-clear-active')
+                .removeClass('is-active')
+            }
+
+            if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
               return;
             }
+
             page = $('body').data('page');
             isIssueIndex = page === 'projects:issues:index';
             isMRIndex = page === 'projects:merge_requests:index';
-            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+            if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+              if (label.isAny) {
+                gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+              }
+              else if ($el.hasClass('is-active')) {
+                gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+              }
+              else {
+                var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+                filters = filters.filter(function (filteredLabel) {
+                  return filteredLabel !== label.title;
+                });
+                gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+              }
+
+              gl.issueBoards.BoardsStore.updateFiltersUrl();
+              e.preventDefault();
+              return;
+            }
+            else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
               if (!$dropdown.hasClass('js-multiselect')) {
                 selectedLabel = label.title;
                 return Issuable.filterResults($dropdown.closest('form'));
               }
-            } else if ($dropdown.hasClass('js-filter-submit')) {
+            }
+            else if ($dropdown.hasClass('js-filter-submit')) {
               return $dropdown.closest('form').submit();
-            } else {
+            }
+            else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+              if ($el.hasClass('is-active')) {
+                gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+                  id: label.id,
+                  title: label.title,
+                  color: label.color[0],
+                  textColor: '#fff'
+                }));
+              }
+              else {
+                var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+                labels = labels.filter(function (selectedLabel) {
+                  return selectedLabel.id !== label.id;
+                });
+                gl.issueBoards.BoardsStore.detail.issue.labels = labels;
+              }
+
+              $loading.fadeIn();
+
+              gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+                .then(function () {
+                  $loading.fadeOut();
+                });
+            }
+            else {
               if ($dropdown.hasClass('js-multiselect')) {
 
-              } else {
+              }
+              else {
                 return saveLabelData();
               }
             }
@@ -338,7 +420,9 @@
       if ($('.selected_issue:checked').length) {
         return;
       }
+      // Remove inputs
       $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
+      // Also restore button text
       return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
     };
 
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index ce472f3bcd0aac3d6e411a4e637295610dc2a1a2..6b4edf02f4d4a665604471500c3ad5931bcef3ad 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var hideEndFade;
 
@@ -10,11 +11,13 @@
   };
 
   $(function() {
-    hideEndFade($('.scrolling-tabs'));
+    var $scrollingTabs = $('.scrolling-tabs');
+
+    hideEndFade($scrollingTabs);
     $(window).off('resize.nav').on('resize.nav', function() {
-      return hideEndFade($('.scrolling-tabs'));
+      return hideEndFade($scrollingTabs);
     });
-    return $('.scrolling-tabs').on('scroll', function(event) {
+    $scrollingTabs.off('scroll').on('scroll', function(event) {
       var $this, currentPosition, maxPosition;
       $this = $(this);
       currentPosition = $this.scrollLeft();
@@ -22,6 +25,23 @@
       $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
       return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
     });
+
+    $scrollingTabs.each(function () {
+      var $this = $(this),
+          scrollingTabWidth = $this.width(),
+          $active = $this.find('.active'),
+          activeWidth = $active.width();
+
+      if ($active.length) {
+        var offset = $active.offset().left + activeWidth;
+
+        if (offset > scrollingTabWidth - 30) {
+          var scrollLeft = scrollingTabWidth / 2;
+          scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+          $this.scrollLeft(scrollLeft);
+        }
+      }
+    });
   });
 
 }).call(this);
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 0000000000000000000000000000000000000000..b1718e89d3deeb018467fed3547fd566b0d3bca2
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,3 @@
+/* eslint-disable */
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index 8d5e52286b7be5206cb7936ae77d5019d113b5c2..e1dfdae97de0f9c027c78dc0e0b237d39bb6d9dc 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,7 +1,7 @@
+/* eslint-disable */
 
 /*= require Chart */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 8ee81804513b831003744d9a772cd7849e029dc9..155e30cc462ffb8f40a6f96b43ae7b655388285d 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -1,7 +1,7 @@
+/* eslint-disable */
 
 /*= require cropper */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 31e6033e75666a5a8f8844f9ebbbb0e6cdde88a1..0c9c278707747270ead7da521ba400d41a49b073 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,7 +1,7 @@
+/* eslint-disable */
 
 /*= require d3 */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index 923c575dcfe637946f7344a714029775ad98f9d5..cc445db274be10713d3b269b0c11c91fc3907dfb 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,13 +1,9 @@
+/* eslint-disable */
 
 /*= require raphael */
-
-
 /*= require g.raphael */
-
-
 /*= require g.bar */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index d36efdabc93854460e5bf15be8c3d1202478e46e..a68edab2aadc3c7c97467172bce279e0a5c3d148 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     if (w.gl == null) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9299d0eabd27f5f7702ec9c1346e3ca09396fd69..6cb3d95f984411813cdeffd475dc76fddb8400ad 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var base;
@@ -23,6 +24,81 @@
         return null;
       }
     };
+
+    w.gl.utils.ajaxGet = function(url) {
+      return $.ajax({
+        type: "GET",
+        url: url,
+        dataType: "script"
+      });
+    };
+
+    w.gl.utils.split = function(val) {
+      return val.split(/,\s*/);
+    };
+
+    w.gl.utils.extractLast = function(term) {
+      return this.split(term).pop();
+    };
+
+    w.gl.utils.rstrip = function rstrip(val) {
+      if (val) {
+        return val.replace(/\s+$/, '');
+      } else {
+        return val;
+      }
+    };
+
+    w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
+      event_name = event_name || 'input';
+      var closest_submit, field, that;
+      that = this;
+      field = $(field_selector);
+      closest_submit = field.closest('form').find(button_selector);
+      if (this.rstrip(field.val()) === "") {
+        closest_submit.disable();
+      }
+      return field.on(event_name, function() {
+        if (that.rstrip($(this).val()) === "") {
+          return closest_submit.disable();
+        } else {
+          return closest_submit.enable();
+        }
+      });
+    };
+
+    w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
+      var closest_submit, updateButtons;
+      closest_submit = form.find(button_selector);
+      updateButtons = function() {
+        var filled;
+        filled = true;
+        form.find('input').filter(form_selector).each(function() {
+          return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required');
+        });
+        if (filled) {
+          return closest_submit.enable();
+        } else {
+          return closest_submit.disable();
+        }
+      };
+      updateButtons();
+      return form.keyup(updateButtons);
+    };
+
+    w.gl.utils.sanitize = function(str) {
+      return str.replace(/<(?:.|\n)*?>/gm, '');
+    };
+
+    w.gl.utils.unbindEvents = function() {
+      return $(document).off('scroll');
+    };
+
+    w.gl.utils.shiftWindow = function() {
+      return w.scrollBy(0, -100);
+    };
+
+
     gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
       return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle');
     };
@@ -38,22 +114,16 @@
     gl.utils.getPagePath = function() {
       return $('body').data('page').split(':')[0];
     };
-    return jQuery.timefor = function(time, suffix, expiredLabel) {
-      var suffixFromNow, timefor;
-      if (!time) {
-        return '';
-      }
-      suffix || (suffix = 'remaining');
-      expiredLabel || (expiredLabel = 'Past due');
-      jQuery.timeago.settings.allowFuture = true;
-      suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow;
-      jQuery.timeago.settings.strings.suffixFromNow = suffix;
-      timefor = $.timeago(time);
-      if (timefor.indexOf('ago') > -1) {
-        timefor = expiredLabel;
-      }
-      jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow;
-      return timefor;
+    gl.utils.parseUrl = function (url) {
+      var parser = document.createElement('a');
+      parser.href = url;
+      return parser;
+    };
+    gl.utils.cleanupBeforeFetch = function() {
+      // Unbind scroll events
+      $(document).off('scroll');
+      // Close any open tooltips
+      $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
     };
   })(window);
 
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 10afa7e432985da0f7eeb3d721e77161ec98b3bd..3965109dd658df90a6c90584ce4666adcfd87124 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var base;
@@ -21,50 +22,72 @@
       if (setTimeago == null) {
         setTimeago = true;
       }
+
       $timeagoEls.each(function() {
-        var $el;
-        $el = $(this);
-        return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+        var $el = $(this);
+        $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+
+        if (setTimeago) {
+          // Recreate with custom template
+          $el.tooltip({
+            template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+          });
+        }
+        gl.utils.renderTimeago($el);
       });
-      if (setTimeago) {
-        $timeagoEls.timeago();
-        $timeagoEls.tooltip('destroy');
-        return $timeagoEls.tooltip({
-          template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
-        });
-      }
     };
 
-    w.gl.utils.shortTimeAgo = function($el) {
-      var shortLocale, tmpLocale;
-      shortLocale = {
-        prefixAgo: null,
-        prefixFromNow: null,
-        suffixAgo: 'ago',
-        suffixFromNow: 'from now',
-        seconds: '1 min',
-        minute: '1 min',
-        minutes: '%d mins',
-        hour: '1 hr',
-        hours: '%d hrs',
-        day: '1 day',
-        days: '%d days',
-        month: '1 month',
-        months: '%d months',
-        year: '1 year',
-        years: '%d years',
-        wordSeparator: ' ',
-        numbers: []
+    w.gl.utils.getTimeago = function() {
+      var locale = function(number, index) {
+        return [
+          ['less than a minute ago', 'a while'],
+          ['less than a minute ago', 'in %s seconds'],
+          ['about a minute ago', 'in 1 minute'],
+          ['%s minutes ago', 'in %s minutes'],
+          ['about an hour ago', 'in 1 hour'],
+          ['about %s hours ago', 'in %s hours'],
+          ['a day ago', 'in 1 day'],
+          ['%s days ago', 'in %s days'],
+          ['a week ago', 'in 1 week'],
+          ['%s weeks ago', 'in %s weeks'],
+          ['a month ago', 'in 1 month'],
+          ['%s months ago', 'in %s months'],
+          ['a year ago', 'in 1 year'],
+          ['%s years ago', 'in %s years']
+        ][index];
       };
-      tmpLocale = $.timeago.settings.strings;
-      $el.each(function(el) {
-        var $el1;
-        $el1 = $(this);
-        return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
-      });
-      $.timeago.settings.strings = shortLocale;
-      $el.timeago();
-      $.timeago.settings.strings = tmpLocale;
+
+      timeago.register('gl_en', locale);
+      return timeago();
+    };
+
+    w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+      var timefor;
+      if (!time) {
+        return '';
+      }
+      suffix || (suffix = 'remaining');
+      expiredLabel || (expiredLabel = 'Past due');
+      timefor = gl.utils.getTimeago().format(time).replace('in', '');
+      if (timefor.indexOf('ago') > -1) {
+        timefor = expiredLabel;
+      } else {
+        timefor = timefor.trim() + ' ' + suffix;
+      }
+      return timefor;
+    };
+
+    w.gl.utils.renderTimeago = function($element) {
+      var timeagoInstance = gl.utils.getTimeago();
+      timeagoInstance.render($element, 'gl_en');
+    };
+
+    w.gl.utils.getDayDifference = function(a, b) {
+      var millisecondsPerDay = 1000 * 60 * 60 * 24;
+      var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+      var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+      return Math.floor((date2 - date1) / millisecondsPerDay);
     };
 
   })(window);
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
deleted file mode 100644
index 80f9936b9c2039ee8e1625128ad862e26d8847e0..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-gl.emojiAliases = ->
-  JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
new file mode 100644
index 0000000000000000000000000000000000000000..aeb86c9fa5bc7758a85242bb7fd2242a6ac02b26
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
@@ -0,0 +1,6 @@
+(function() {
+  gl.emojiAliases = function() {
+    return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
+  };
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/jquery.timeago.js b/app/assets/javascripts/lib/utils/jquery.timeago.js
deleted file mode 100644
index cc17aa7d3d1883a93d53b2f192415cf9042e2461..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/lib/utils/jquery.timeago.js
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * Timeago is a jQuery plugin that makes it easy to support automatically
- * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
- *
- * @name timeago
- * @version 1.1.0
- * @requires jQuery v1.2.3+
- * @author Ryan McGeary
- * @license MIT License - http://www.opensource.org/licenses/mit-license.php
- *
- * For usage and examples, visit:
- * http://timeago.yarp.com/
- *
- * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
- */
-
-(function (factory) {
-  if (typeof define === 'function' && define.amd) {
-    // AMD. Register as an anonymous module.
-    define(['jquery'], factory);
-  } else {
-    // Browser globals
-    factory(jQuery);
-  }
-}(function ($) {
-  $.timeago = function(timestamp) {
-    if (timestamp instanceof Date) {
-      return inWords(timestamp);
-    } else if (typeof timestamp === "string") {
-      return inWords($.timeago.parse(timestamp));
-    } else if (typeof timestamp === "number") {
-      return inWords(new Date(timestamp));
-    } else {
-      return inWords($.timeago.datetime(timestamp));
-    }
-  };
-  var $t = $.timeago;
-
-  $.extend($.timeago, {
-    settings: {
-      refreshMillis: 60000,
-      allowFuture: false,
-      strings: {
-        prefixAgo: null,
-        prefixFromNow: null,
-        suffixAgo: "ago",
-        suffixFromNow: "from now",
-        seconds: "less than a minute",
-        minute: "about a minute",
-        minutes: "%d minutes",
-        hour: "about an hour",
-        hours: "about %d hours",
-        day: "a day",
-        days: "%d days",
-        month: "about a month",
-        months: "%d months",
-        year: "about a year",
-        years: "%d years",
-        wordSeparator: " ",
-        numbers: []
-      }
-    },
-    inWords: function(distanceMillis) {
-      var $l = this.settings.strings;
-      var prefix = $l.prefixAgo;
-      var suffix = $l.suffixAgo;
-      if (this.settings.allowFuture) {
-        if (distanceMillis < 0) {
-          prefix = $l.prefixFromNow;
-          suffix = $l.suffixFromNow;
-        }
-      }
-
-      var seconds = Math.abs(distanceMillis) / 1000;
-      var minutes = seconds / 60;
-      var hours = minutes / 60;
-      var days = hours / 24;
-      var years = days / 365;
-
-      function substitute(stringOrFunction, number) {
-        var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
-        var value = ($l.numbers && $l.numbers[number]) || number;
-        return string.replace(/%d/i, value);
-      }
-
-      var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
-        seconds < 90 && substitute($l.minute, 1) ||
-        minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
-        minutes < 90 && substitute($l.hour, 1) ||
-        hours < 24 && substitute($l.hours, Math.round(hours)) ||
-        hours < 42 && substitute($l.day, 1) ||
-        days < 30 && substitute($l.days, Math.round(days)) ||
-        days < 45 && substitute($l.month, 1) ||
-        days < 365 && substitute($l.months, Math.round(days / 30)) ||
-        years < 1.5 && substitute($l.year, 1) ||
-        substitute($l.years, Math.round(years));
-
-      var separator = $l.wordSeparator || "";
-      if ($l.wordSeparator === undefined) { separator = " "; }
-      return $.trim([prefix, words, suffix].join(separator));
-    },
-    parse: function(iso8601) {
-      var s = $.trim(iso8601);
-      s = s.replace(/\.\d+/,""); // remove milliseconds
-      s = s.replace(/-/,"/").replace(/-/,"/");
-      s = s.replace(/T/," ").replace(/Z/," UTC");
-      s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
-      return new Date(s);
-    },
-    datetime: function(elem) {
-      var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
-      return $t.parse(iso8601);
-    },
-    isTime: function(elem) {
-      // jQuery's `is()` doesn't play well with HTML5 in IE
-      return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
-    }
-  });
-
-  // functions that can be called via $(el).timeago('action')
-  // init is default when no action is given
-  // functions are called with context of a single element
-  var functions = {
-    init: function(){
-      var refresh_el = $.proxy(refresh, this);
-      refresh_el();
-      var $s = $t.settings;
-      if ($s.refreshMillis > 0) {
-        setInterval(refresh_el, $s.refreshMillis);
-      }
-    },
-    update: function(time){
-      $(this).data('timeago', { datetime: $t.parse(time) });
-      refresh.apply(this);
-    }
-  };
-
-  $.fn.timeago = function(action, options) {
-    var fn = action ? functions[action] : functions.init;
-    if(!fn){
-      throw new Error("Unknown function name '"+ action +"' for timeago");
-    }
-    // each over objects here and call the requested function
-    this.each(function(){
-      fn.call(this, options);
-    });
-    return this;
-  };
-
-  function refresh() {
-    var data = prepareData(this);
-    if (!isNaN(data.datetime)) {
-      $(this).text(inWords(data.datetime));
-    }
-    return this;
-  }
-
-  function prepareData(element) {
-    element = $(element);
-    if (!element.data("timeago")) {
-      element.data("timeago", { datetime: $t.datetime(element) });
-      var text = $.trim(element.text());
-      if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
-        element.attr("title", text);
-      }
-    }
-    return element.data("timeago");
-  }
-
-  function inWords(date) {
-    return $t.inWords(distance(date));
-  }
-
-  function distance(date) {
-    return (new Date().getTime() - date.getTime());
-  }
-
-  // fix for IE6 suckage
-  document.createElement("abbr");
-  document.createElement("time");
-}));
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 42b6ac0589ed3ac8361dee2a34be4e80f62b3516..dafc006d2e591b491c28c93e6efe0869302b47f5 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var notificationGranted, notifyMe, notifyPermissions;
@@ -6,6 +7,7 @@
       notification = new Notification(message, opts);
       setTimeout(function() {
         return notification.close();
+      // Hide the notification after X amount of seconds
       }, 8000);
       if (onclick) {
         return notification.onclick = onclick;
@@ -22,12 +24,16 @@
         body: body,
         icon: icon
       };
+      // Let's check if the browser supports notifications
       if (!('Notification' in window)) {
 
+      // do nothing
       } else if (Notification.permission === 'granted') {
+        // If it's okay let's create a notification
         return notificationGranted(message, opts, onclick);
       } else if (Notification.permission !== 'denied') {
         return Notification.requestPermission(function(permission) {
+          // If the user accepts, let's create a notification
           if (permission === 'granted') {
             return notificationGranted(message, opts, onclick);
           }
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 130479642f32deb27ed79e278bce1df8465cbfcb..98f9815ff05c4f70e285627a0352b463a7fd9845 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var base;
@@ -7,6 +8,9 @@
     if ((base = w.gl).text == null) {
       base.text = {};
     }
+    gl.text.addDelimiter = function(text) {
+      return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
+    }
     gl.text.randomString = function() {
       return Math.random().toString(36).substring(7);
     };
@@ -29,6 +33,7 @@
       lineBefore = this.lineBefore(text, textArea);
       lineAfter = this.lineAfter(text, textArea);
       if (lineBefore === blockTag && lineAfter === blockTag) {
+        // To remove the block tag we have to select the line before & after
         if (blockTag != null) {
           textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
           textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
@@ -63,11 +68,11 @@
       if (!inserted) {
         try {
           document.execCommand("ms-beginUndoUnit");
-        } catch (undefined) {}
+        } catch (error) {}
         textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
         try {
           document.execCommand("ms-endUndoUnit");
-        } catch (undefined) {}
+        } catch (error) {}
       }
       return this.moveCursor(textArea, tag, wrap);
     };
@@ -104,9 +109,12 @@
         return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
       });
     };
-    return gl.text.removeListeners = function(form) {
+    gl.text.removeListeners = function(form) {
       return $('.js-md', form).off();
     };
+    return gl.text.truncate = function(string, maxLength) {
+      return string.substr(0, (maxLength - 3)) + '...';
+    };
   })(window);
 
 }).call(this);
diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js
new file mode 100644
index 0000000000000000000000000000000000000000..42606dd2d4600974cb0eb2b1f2f84a00c0dac051
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/timeago.js
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2016 hustcc
+ * License: MIT
+ * Version: v2.0.2
+ * https://github.com/hustcc/timeago.js
+ * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
+**/
+/* eslint-disable */
+/* jshint expr: true */
+!function (root, factory) {
+  if (typeof module === 'object' && module.exports)
+    module.exports = factory(root);
+  else
+    root.timeago = factory(root);
+}(typeof window !== 'undefined' ? window : this, 
+function () {
+  var cnt = 0, // the timer counter, for timer key
+    indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
+
+    // build-in locales: en & zh_CN
+    locales = {
+      'en': function(number, index) {
+        if (index === 0) return ['just now', 'right now'];
+        var unit = indexMapEn[parseInt(index / 2)];
+        if (number > 1) unit += 's';
+        return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
+      },
+    },
+    // second, minute, hour, day, week, month, year(365 days)
+    SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
+    SEC_ARRAY_LEN = 6,
+    ATTR_DATETIME = 'datetime';
+  
+  // format Date / string / timestamp to Date instance.
+  function toDate(input) {
+    if (input instanceof Date) return input;
+    if (!isNaN(input)) return new Date(toInt(input));
+    if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
+    input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
+      .replace(/-/, '/').replace(/-/, '/')
+      .replace(/T/, ' ').replace(/Z/, ' UTC')
+      .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
+    return new Date(input);
+  }
+  // change f into int, remove Decimal. just for code compression
+  function toInt(f) {
+    return parseInt(f);
+  }
+  // format the diff second to *** time ago, with setting locale
+  function formatDiff(diff, locale, defaultLocale) {
+    // if locale is not exist, use defaultLocale.
+    // if defaultLocale is not exist, use build-in `en`.
+    // be sure of no error when locale is not exist.
+    locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
+    // if (! locales[locale]) locale = defaultLocale;
+    var i = 0;
+      agoin = diff < 0 ? 1 : 0; // timein or timeago
+    diff = Math.abs(diff);
+
+    for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+      diff /= SEC_ARRAY[i];
+    }
+    diff = toInt(diff);
+    i *= 2;
+
+    if (diff > (i === 0 ? 9 : 1)) i += 1;
+    return locales[locale](diff, i)[agoin].replace('%s', diff);
+  }
+  // calculate the diff second between date to be formated an now date.
+  function diffSec(date, nowDate) {
+    nowDate = nowDate ? toDate(nowDate) : new Date();
+    return (nowDate - toDate(date)) / 1000;
+  }
+  /**
+   * nextInterval: calculate the next interval time.
+   * - diff: the diff sec between now and date to be formated.
+   *
+   * What's the meaning?
+   * diff = 61 then return 59
+   * diff = 3601 (an hour + 1 second), then return 3599
+   * make the interval with high performace.
+  **/
+  function nextInterval(diff) {
+    var rst = 1, i = 0, d = Math.abs(diff);
+    for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+      diff /= SEC_ARRAY[i];
+      rst *= SEC_ARRAY[i];
+    }
+    // return leftSec(d, rst);
+    d = d % rst;
+    d = d ? rst - d : rst;
+    return Math.ceil(d);
+  }
+  // get the datetime attribute, jQuery and DOM
+  function getDateAttr(node) {
+    if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
+    if(node.attr) return node.attr(ATTR_DATETIME);
+  }
+  /**
+   * timeago: the function to get `timeago` instance.
+   * - nowDate: the relative date, default is new Date().
+   * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
+   *
+   * How to use it?
+   * var timeagoLib = require('timeago.js');
+   * var timeago = timeagoLib(); // all use default.
+   * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
+   * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
+   * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
+  **/
+  function Timeago(nowDate, defaultLocale) {
+    var timers = {}; // real-time render timers
+    // if do not set the defaultLocale, set it with `en`
+    if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
+    // what the timer will do
+    function doRender(node, date, locale, cnt) {
+      var diff = diffSec(date, nowDate);
+      node.innerHTML = formatDiff(diff, locale, defaultLocale);
+      // waiting %s seconds, do the next render
+      timers['k' + cnt] = setTimeout(function() {
+        doRender(node, date, locale, cnt);
+      }, nextInterval(diff) * 1000);
+    }
+    /**
+     * nextInterval: calculate the next interval time.
+     * - diff: the diff sec between now and date to be formated.
+     *
+     * What's the meaning?
+     * diff = 61 then return 59
+     * diff = 3601 (an hour + 1 second), then return 3599
+     * make the interval with high performace.
+    **/
+    // this.nextInterval = function(diff) { // for dev test
+    //   var rst = 1, i = 0, d = Math.abs(diff);
+    //   for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+    //     diff /= SEC_ARRAY[i];
+    //     rst *= SEC_ARRAY[i];
+    //   }
+    //   // return leftSec(d, rst);
+    //   d = d % rst;
+    //   d = d ? rst - d : rst;
+    //   return Math.ceil(d);
+    // }; // for dev test
+    /**
+     * format: format the date to *** time ago, with setting or default locale
+     * - date: the date / string / timestamp to be formated
+     * - locale: the formated string's locale name, e.g. en / zh_CN
+     *
+     * How to use it?
+     * var timeago = require('timeago.js')();
+     * timeago.format(new Date(), 'pl'); // Date instance
+     * timeago.format('2016-09-10', 'fr'); // formated date string
+     * timeago.format(1473473400269); // timestamp with ms
+    **/
+    this.format = function(date, locale) {
+      return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
+    };
+    /**
+     * render: render the DOM real-time.
+     * - nodes: which nodes will be rendered.
+     * - locale: the locale name used to format date.
+     *
+     * How to use it?
+     * var timeago = new require('timeago.js')();
+     * // 1. javascript selector
+     * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
+     * // 2. use jQuery selector
+     * timeago.render($('.need_to_be_rendered'), 'pl');
+     *
+     * Notice: please be sure the dom has attribute `datetime`.
+    **/
+    this.render = function(nodes, locale) {
+      if (nodes.length === undefined) nodes = [nodes];
+      for (var i = 0; i < nodes.length; i++) {
+        doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
+      }
+    };
+    /**
+     * cancel: cancel all the timers which are doing real-time render.
+     *
+     * How to use it?
+     * var timeago = new require('timeago.js')();
+     * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
+     * timeago.cancel(); // will stop all the timer, stop render in real time.
+    **/
+    this.cancel = function() {
+      for (var key in timers) {
+        clearTimeout(timers[key]);
+      }
+      timers = {};
+    };
+    /**
+     * setLocale: set the default locale name.
+     *
+     * How to use it?
+     * var timeago = require('timeago.js');
+     * timeago = new timeago();
+     * timeago.setLocale('fr');
+    **/
+    this.setLocale = function(locale) {
+      defaultLocale = locale;
+    };
+    return this;
+  }
+  /**
+   * timeago: the function to get `timeago` instance.
+   * - nowDate: the relative date, default is new Date().
+   * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
+   *
+   * How to use it?
+   * var timeagoLib = require('timeago.js');
+   * var timeago = timeagoLib(); // all use default.
+   * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
+   * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
+   * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
+   **/
+  function timeagoFactory(nowDate, defaultLocale) {
+    return new Timeago(nowDate, defaultLocale);
+  }
+  /**
+   * register: register a new language locale
+   * - locale: locale name, e.g. en / zh_CN, notice the standard.
+   * - localeFunc: the locale process function
+   *
+   * How to use it?
+   * var timeagoLib = require('timeago.js');
+   *
+   * timeagoLib.register('the locale name', the_locale_func);
+   * // or
+   * timeagoLib.register('pl', require('timeago.js/locales/pl'));
+   **/
+  timeagoFactory.register = function(locale, localeFunc) {
+    locales[locale] = localeFunc;
+  };
+
+  return timeagoFactory;
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index dc30babd645d9d5f4b3be00fd8af55b7abf8eba4..4fd1e3fc1d300b73a8813ada4cc50ff0a0fd8c17 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var base;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index fffbfd19745d7887c2533f130ef3ecefb44a886e..44a66a915e37a00f0f28974c3f04ab7ff756ae35 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   (function(w) {
     var base;
@@ -7,6 +8,8 @@
     if ((base = w.gl).utils == null) {
       base.utils = {};
     }
+    // Returns an array containing the value(s) of the
+    // of the key passed as an argument
     w.gl.utils.getParameterValues = function(sParam) {
       var i, sPageURL, sParameterName, sURLVariables, values;
       sPageURL = decodeURIComponent(window.location.search.substring(1));
@@ -17,12 +20,14 @@
       while (i < sURLVariables.length) {
         sParameterName = sURLVariables[i].split('=');
         if (sParameterName[0] === sParam) {
-          values.push(sParameterName[1]);
+          values.push(sParameterName[1].replace(/\+/g, ' '));
         }
         i++;
       }
       return values;
     };
+    // @param {Object} params - url keys and value to merge
+    // @param {String} url
     w.gl.utils.mergeUrlParams = function(params, url) {
       var lastChar, newUrl, paramName, paramValue, pattern;
       newUrl = decodeURIComponent(url);
@@ -37,13 +42,15 @@
           newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
         }
       }
+      // Remove a trailing ampersand
       lastChar = newUrl[newUrl.length - 1];
       if (lastChar === '&') {
         newUrl = newUrl.slice(0, -1);
       }
       return newUrl;
     };
-    return w.gl.utils.removeParamQueryString = function(url, param) {
+    // removes parameter query string from url. returns the modified url
+    w.gl.utils.removeParamQueryString = function(url, param) {
       var urlVariables, variables;
       url = decodeURIComponent(url);
       urlVariables = url.split('&');
@@ -59,6 +66,16 @@
         return results;
       })()).join('&');
     };
+    w.gl.utils.getLocationHash = function(url) {
+      var hashIndex;
+      if (typeof url === 'undefined') {
+        // Note: We can't use window.location.hash here because it's
+        // not consistent across browsers - Firefox will pre-decode it
+        url = window.location.href;
+      }
+      hashIndex = url.indexOf('#');
+      return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+    };
   })(window);
 
 }).call(this);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f145bd3ad74a4658b25a34a12409930d844ada8d..ea5a60bb78e1369f38c7504860a474939cb09ba7 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,17 +1,50 @@
-
+/* eslint-disable */
+// LineHighlighter
+//
+// Handles single- and multi-line selection and highlight for blob views.
+//
 /*= require jquery.scrollTo */
 
+//
+// ### Example Markup
+//
+//   <div id="blob-content-holder">
+//     <div class="file-content">
+//       <div class="line-numbers">
+//         <a href="#L1" id="L1" data-line-number="1">1</a>
+//         <a href="#L2" id="L2" data-line-number="2">2</a>
+//         <a href="#L3" id="L3" data-line-number="3">3</a>
+//         <a href="#L4" id="L4" data-line-number="4">4</a>
+//         <a href="#L5" id="L5" data-line-number="5">5</a>
+//       </div>
+//       <pre class="code highlight">
+//         <code>
+//           <span id="LC1" class="line">...</span>
+//           <span id="LC2" class="line">...</span>
+//           <span id="LC3" class="line">...</span>
+//           <span id="LC4" class="line">...</span>
+//           <span id="LC5" class="line">...</span>
+//         </code>
+//       </pre>
+//     </div>
+//   </div>
+//
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
   this.LineHighlighter = (function() {
+    // CSS class applied to highlighted lines
     LineHighlighter.prototype.highlightClass = 'hll';
 
+    // Internal copy of location.hash so we're not dependent on `location` in tests
     LineHighlighter.prototype._hash = '';
 
     function LineHighlighter(hash) {
       var range;
       if (hash == null) {
+        // Initialize a LineHighlighter object
+        //
+        // hash - String URL hash for dependency injection in tests
         hash = location.hash;
       }
       this.setHash = bind(this.setHash, this);
@@ -24,6 +57,8 @@
         if (range[0]) {
           this.highlightRange(range);
           $.scrollTo("#L" + range[0], {
+            // Scroll to the first highlighted line on initial load
+            // Offset -50 for the sticky top bar, and another -100 for some context
             offset: -150
           });
         }
@@ -32,6 +67,12 @@
 
     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.
       return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
         return event.preventDefault();
       });
@@ -44,6 +85,8 @@
       lineNumber = $(event.target).closest('a').data('line-number');
       current = this.hashToRange(this._hash);
       if (!(current[0] && event.shiftKey)) {
+        // If there's no current selection, or there is but Shift wasn't held,
+        // treat this like a single-line selection.
         this.setHash(lineNumber);
         return this.highlightLine(lineNumber);
       } else if (event.shiftKey) {
@@ -59,10 +102,23 @@
 
     LineHighlighter.prototype.clearHighlight = function() {
       return $("." + this.highlightClass).removeClass(this.highlightClass);
+    // Unhighlight previously highlighted lines
     };
 
+    // Convert a URL hash String into line numbers
+    //
+    // hash - Hash String
+    //
+    // Examples:
+    //
+    //   hashToRange('#L5')    # => [5, null]
+    //   hashToRange('#L5-15') # => [5, 15]
+    //   hashToRange('#foo')   # => [null, null]
+    //
+    // Returns an Array
     LineHighlighter.prototype.hashToRange = function(hash) {
       var first, last, matches;
+      //?L(\d+)(?:-(\d+))?$/)
       matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
       if (matches && matches.length) {
         first = parseInt(matches[1]);
@@ -73,10 +129,16 @@
       }
     };
 
+    // Highlight a single line
+    //
+    // lineNumber - Line number to highlight
     LineHighlighter.prototype.highlightLine = function(lineNumber) {
       return $("#LC" + lineNumber).addClass(this.highlightClass);
     };
 
+    // Highlight all lines within a range
+    //
+    // range - Array containing the starting and ending line numbers
     LineHighlighter.prototype.highlightRange = function(range) {
       var i, lineNumber, ref, ref1, results;
       if (range[1]) {
@@ -90,6 +152,7 @@
       }
     };
 
+    // Set the URL hash string
     LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
       var hash;
       if (lastLineNumber) {
@@ -101,10 +164,15 @@
       return this.__setLocationHash__(hash);
     };
 
+    // Make the actual hash change in the browser
+    //
+    // This method is stubbed in tests.
     LineHighlighter.prototype.__setLocationHash__ = function(value) {
       return history.pushState({
         turbolinks: false,
         url: value
+      // We're using pushState instead of assigning location.hash directly to
+      // prevent the page from scrolling on the hashchange event
       }, document.title, value);
     };
 
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 218f24fe908cdcb3debbdc1f1487f173d9980c1c..d4f86534f0cef3c21dbc6de14d9819710c4691c3 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,54 +1,13 @@
+/* eslint-disable */
 (function() {
-  var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work;
-
   Turbolinks.enableProgressBar();
 
-  defaultClass = 'tanuki-shape';
-
-  pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek'];
-
-  pieceIndex = 0;
-
-  firstPiece = pieces[0];
-
-  currentTimer = null;
-
-  delay = 150;
-
-  clearHighlights = function() {
-    return $("." + defaultClass + ".highlight").attr('class', defaultClass);
-  };
-
-  start = function() {
-    clearHighlights();
-    pieceIndex = 0;
-    if (pieces[0] !== firstPiece) {
-      pieces.reverse();
-    }
-    if (currentTimer) {
-      clearInterval(currentTimer);
-    }
-    return currentTimer = setInterval(work, delay);
-  };
-
-  stop = function() {
-    clearInterval(currentTimer);
-    return clearHighlights();
-  };
-
-  work = function() {
-    clearHighlights();
-    $(pieces[pieceIndex]).attr('class', defaultClass + " highlight");
-    if (pieceIndex === pieces.length - 1) {
-      pieceIndex = 0;
-      return pieces.reverse();
-    } else {
-      return pieceIndex++;
-    }
-  };
-
-  $(document).on('page:fetch', start);
+  $(document).on('page:fetch', function() {
+    $('.tanuki-logo').addClass('animate');
+  });
 
-  $(document).on('page:change', stop);
+  $(document).on('page:change', function() {
+    $('.tanuki-logo').removeClass('animate');
+  });
 
 }).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 0000000000000000000000000000000000000000..0bd90c573967627b5d67211dbea1348a96993df5
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,37 @@
+/* eslint-disable */
+(function() {
+  // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+  // children of an element with the `clearable-input` class, and have a sibling
+  // `js-clear-input` element, then show that element when there is a value in the
+  // datepicker, and make clicking on that element clear the field.
+  //
+  gl.MemberExpirationDate = function() {
+    function toggleClearInput() {
+      $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+    }
+
+    var inputs = $('.js-access-expiration-date');
+
+    inputs.datepicker({
+      dateFormat: 'yy-mm-dd',
+      minDate: 1,
+      onSelect: function () {
+        $(this).trigger('change');
+        toggleClearInput.call(this);
+      }
+    });
+
+    inputs.next('.js-clear-input').on('click', function(event) {
+      event.preventDefault();
+
+      var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+      input.datepicker('setDate', null)
+        .trigger('change');
+      toggleClearInput.call(input);
+    });
+
+    inputs.on('blur', toggleClearInput);
+
+    inputs.each(toggleClearInput);
+  };
+}).call(this);
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..895bc10784ff5ff18439ae17279a5efc1560090d
--- /dev/null
+++ b/app/assets/javascripts/members.js.es6
@@ -0,0 +1,38 @@
+/* eslint-disable */
+((w) => {
+  w.gl = w.gl || {};
+
+  class Members {
+    constructor() {
+      this.addListeners();
+    }
+
+    addListeners() {
+      $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
+      $('.js-member-update-control').off('change').on('change', this.formSubmit);
+      $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+      gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
+    }
+
+    removeRow(e) {
+      const $target = $(e.target);
+
+      if ($target.hasClass('btn-remove')) {
+        $target.closest('.member')
+          .fadeOut(function () {
+            $(this).remove();
+          });
+      }
+    }
+
+    formSubmit() {
+      $(this).closest('form').trigger("submit.rails").end().disable();
+    }
+
+    formSuccess() {
+      $(this).find('.js-member-update-control').enable();
+    }
+  }
+
+  gl.Members = Members;
+})(window);
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..6da3942ea5249e562842930eec82fb908cdbeb2d
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -0,0 +1,94 @@
+/* eslint-disable */
+((global) => {
+
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.diffFileEditor = Vue.extend({
+    props: {
+      file: Object,
+      onCancelDiscardConfirmation: Function,
+      onAcceptDiscardConfirmation: Function
+    },
+    data() {
+      return {
+        saved: false,
+        loading: false,
+        fileLoaded: false,
+        originalContent: '',
+      }
+    },
+    computed: {
+      classObject() {
+        return {
+          'saved': this.saved,
+          'is-loading': this.loading
+        };
+      }
+    },
+    watch: {
+      ['file.showEditor'](val) {
+        this.resetEditorContent();
+
+        if (!val || this.fileLoaded || this.loading) {
+          return;
+        }
+
+        this.loadEditor();
+      }
+    },
+    ready() {
+      if (this.file.loadEditor) {
+        this.loadEditor();
+      }
+    },
+    methods: {
+      loadEditor() {
+        this.loading = true;
+
+        $.get(this.file.content_path)
+          .done((file) => {
+            let content = this.$el.querySelector('pre');
+            let fileContent = document.createTextNode(file.content);
+
+            content.textContent = fileContent.textContent;
+
+            this.originalContent = file.content;
+            this.fileLoaded = true;
+            this.editor = ace.edit(content);
+            this.editor.$blockScrolling = Infinity; // Turn off annoying warning
+            this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`);
+            this.editor.on('change', () => {
+              this.saveDiffResolution();
+            });
+            this.saveDiffResolution();
+          })
+          .fail(() => {
+            new Flash('Failed to load the file, please try again.');
+          })
+          .always(() => {
+            this.loading = false;
+          });
+      },
+      saveDiffResolution() {
+        this.saved = true;
+
+        // This probably be better placed in the data provider
+        this.file.content = this.editor.getValue();
+        this.file.resolveEditChanged = this.file.content !== this.originalContent;
+        this.file.promptDiscardConfirmation = false;
+      },
+      resetEditorContent() {
+        if (this.fileLoaded) {
+          this.editor.setValue(this.originalContent, -1);
+        }
+      },
+      cancelDiscardConfirmation(file) {
+        this.onCancelDiscardConfirmation(file);
+      },
+      acceptDiscardConfirmation(file) {
+        this.onAcceptDiscardConfirmation(file);
+      }
+    }
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..23c4618af7048852a8a83ef39634d445ba8bae1d
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -0,0 +1,13 @@
+/* eslint-disable */
+((global) => {
+
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.inlineConflictLines = Vue.extend({
+    props: {
+      file: Object
+    },
+    mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..797850262cc9e69460309f9bdbfbbb8d8814ede1
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
@@ -0,0 +1,15 @@
+/* eslint-disable */
+((global) => {
+
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.parallelConflictLine = Vue.extend({
+    props: {
+      file: Object,
+      line: Object
+    },
+    mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+    template: '#parallel-conflict-line'
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..1b3e9901f1ed6f82cd828752725b62a8b76d90f0
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -0,0 +1,16 @@
+/* eslint-disable */
+((global) => {
+
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.parallelConflictLines = Vue.extend({
+    props: {
+      file: Object
+    },
+    mixins: [global.mergeConflicts.utils],
+    components: {
+      'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
+    }
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..8a7519b07869e540e1989568c6383b9a63b6aa36
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -0,0 +1,31 @@
+/* eslint-disable */
+((global) => {
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  class mergeConflictsService {
+    constructor(options) {
+      this.conflictsPath = options.conflictsPath;
+      this.resolveConflictsPath = options.resolveConflictsPath;
+    }
+
+    fetchConflictsData() {
+      return $.ajax({
+        dataType: 'json',
+        url: this.conflictsPath
+      });
+    }
+
+    submitResolveConflicts(data) {
+      return $.ajax({
+        url: this.resolveConflictsPath,
+        data: JSON.stringify(data),
+        contentType: 'application/json',
+        dataType: 'json',
+        method: 'POST'
+      });
+    }
+  };
+
+  global.mergeConflicts.mergeConflictsService = mergeConflictsService;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..f94e51e783cbfe88d6214e8f65006b642c39fc84
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -0,0 +1,436 @@
+/* eslint-disable */
+((global) => {
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  const diffViewType = Cookies.get('diff_view');
+  const HEAD_HEADER_TEXT = 'HEAD//our changes';
+  const ORIGIN_HEADER_TEXT = 'origin//their changes';
+  const HEAD_BUTTON_TITLE = 'Use ours';
+  const ORIGIN_BUTTON_TITLE = 'Use theirs';
+  const INTERACTIVE_RESOLVE_MODE = 'interactive';
+  const EDIT_RESOLVE_MODE = 'edit';
+  const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
+  const VIEW_TYPES = {
+    INLINE: 'inline',
+    PARALLEL: 'parallel'
+  };
+  const CONFLICT_TYPES = {
+    TEXT: 'text',
+    TEXT_EDITOR: 'text-editor'
+  };
+
+  global.mergeConflicts.mergeConflictsStore = {
+    state: {
+      isLoading: true,
+      hasError: false,
+      isSubmitting: false,
+      isParallel: diffViewType === VIEW_TYPES.PARALLEL,
+      diffViewType: diffViewType,
+      conflictsData: {}
+    },
+
+    setConflictsData(data) {
+      this.decorateFiles(data.files);
+
+      this.state.conflictsData = {
+        files: data.files,
+        commitMessage: data.commit_message,
+        sourceBranch: data.source_branch,
+        targetBranch: data.target_branch,
+        commitMessage: data.commit_message,
+        shortCommitSha: data.commit_sha.slice(0, 7),
+      };
+    },
+
+    decorateFiles(files) {
+      files.forEach((file) => {
+        file.content = '';
+        file.resolutionData = {};
+        file.promptDiscardConfirmation = false;
+        file.resolveMode = DEFAULT_RESOLVE_MODE;
+        file.filePath = this.getFilePath(file);
+        file.iconClass = `fa-${file.blob_icon}`;
+        file.blobPath = file.blob_path;
+
+        if (file.type === CONFLICT_TYPES.TEXT) {
+          file.showEditor = false;
+          file.loadEditor = false;
+
+          this.setInlineLine(file);
+          this.setParallelLine(file);
+        } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
+          file.showEditor = true;
+          file.loadEditor = true;
+        }
+      });
+    },
+
+    setInlineLine(file) {
+      file.inlineLines = [];
+
+      file.sections.forEach((section) => {
+        let currentLineType = 'new';
+        const { conflict, lines, id } = section;
+
+        if (conflict) {
+          file.inlineLines.push(this.getHeadHeaderLine(id));
+        }
+
+        lines.forEach((line) => {
+          const { type } = line;
+
+          if ((type === 'new' || type === 'old') && currentLineType !== type) {
+            currentLineType = type;
+            file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
+          }
+
+          this.decorateLineForInlineView(line, id, conflict);
+          file.inlineLines.push(line);
+        })
+
+        if (conflict) {
+          file.inlineLines.push(this.getOriginHeaderLine(id));
+        }
+      });
+    },
+
+    setParallelLine(file) {
+      file.parallelLines = [];
+      const linesObj = { left: [], right: [] };
+
+      file.sections.forEach((section) => {
+        const { conflict, lines, id } = section;
+
+        if (conflict) {
+          linesObj.left.push(this.getOriginHeaderLine(id));
+          linesObj.right.push(this.getHeadHeaderLine(id));
+        }
+
+        lines.forEach((line) => {
+          const { type } = line;
+
+          if (conflict) {
+            if (type === 'old') {
+              linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
+            } else if (type === 'new') {
+              linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
+            }
+          } else {
+            const lineType = type || 'context';
+
+            linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+            linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
+          }
+        });
+
+        this.checkLineLengths(linesObj);
+      });
+
+      for (let i = 0, len = linesObj.left.length; i < len; i++) {
+        file.parallelLines.push([
+          linesObj.right[i],
+          linesObj.left[i]
+        ]);
+      }
+    },
+
+    setLoadingState(state) {
+      this.state.isLoading = state;
+    },
+
+    setErrorState(state) {
+      this.state.hasError = state;
+    },
+
+    setFailedRequest(message) {
+      this.state.hasError = true;
+      this.state.conflictsData.errorMessage = message;
+    },
+
+    getConflictsCount() {
+      if (!this.state.conflictsData.files.length) {
+        return 0;
+      }
+
+      const files = this.state.conflictsData.files;
+      let count = 0;
+
+      files.forEach((file) => {
+        if (file.type === CONFLICT_TYPES.TEXT) {
+          file.sections.forEach((section) => {
+            if (section.conflict) {
+              count++;
+            }
+          });
+        } else {
+          count++;
+        }
+      });
+
+      return count;
+    },
+
+    getConflictsCountText() {
+      const count = this.getConflictsCount();
+      const text = count ? 'conflicts' : 'conflict';
+
+      return `${count} ${text}`;
+    },
+
+    setViewType(viewType) {
+      this.state.diffView = viewType;
+      this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
+
+      Cookies.set('diff_view', viewType);
+    },
+
+    getHeadHeaderLine(id) {
+      return {
+        id: id,
+        richText: HEAD_HEADER_TEXT,
+        buttonTitle: HEAD_BUTTON_TITLE,
+        type: 'new',
+        section: 'head',
+        isHeader: true,
+        isHead: true,
+        isSelected: false,
+        isUnselected: false
+      };
+    },
+
+    decorateLineForInlineView(line, id, conflict) {
+      const { type } = line;
+      line.id = id;
+      line.hasConflict = conflict;
+      line.isHead = type === 'new';
+      line.isOrigin = type === 'old';
+      line.hasMatch = type === 'match';
+      line.richText = line.rich_text;
+      line.isSelected = false;
+      line.isUnselected = false;
+    },
+
+    getLineForParallelView(line, id, lineType, isHead) {
+      const { old_line, new_line, rich_text } = line;
+      const hasConflict = lineType === 'conflict';
+
+      return {
+        id,
+        lineType,
+        hasConflict,
+        isHead: hasConflict && isHead,
+        isOrigin: hasConflict && !isHead,
+        hasMatch: lineType === 'match',
+        lineNumber: isHead ? new_line : old_line,
+        section: isHead ? 'head' : 'origin',
+        richText: rich_text,
+        isSelected: false,
+        isUnselected: false
+      };
+    },
+
+    getOriginHeaderLine(id) {
+      return {
+        id: id,
+        richText: ORIGIN_HEADER_TEXT,
+        buttonTitle: ORIGIN_BUTTON_TITLE,
+        type: 'old',
+        section: 'origin',
+        isHeader: true,
+        isOrigin: true,
+        isSelected: false,
+        isUnselected: false
+      };
+    },
+
+    getFilePath(file) {
+      const { old_path, new_path } = file;
+      return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+    },
+
+    checkLineLengths(linesObj) {
+      let { left, right } = linesObj;
+
+      if (left.length !== right.length) {
+        if (left.length > right.length) {
+          const diff = left.length - right.length;
+          for (let i = 0; i < diff; i++) {
+            right.push({ lineType: 'emptyLine', richText: '' });
+          }
+        } else {
+          const diff = right.length - left.length;
+          for (let i = 0; i < diff; i++) {
+            left.push({ lineType: 'emptyLine', richText: '' });
+          }
+        }
+      }
+    },
+
+    setPromptConfirmationState(file, state) {
+      file.promptDiscardConfirmation = state;
+    },
+
+    setFileResolveMode(file, mode) {
+      if (mode === INTERACTIVE_RESOLVE_MODE) {
+        file.showEditor = false;
+      } else if (mode === EDIT_RESOLVE_MODE) {
+        // Restore Interactive mode when switching to Edit mode
+        file.showEditor = true;
+        file.loadEditor = true;
+        file.resolutionData = {};
+
+        this.restoreFileLinesState(file);
+      }
+
+      file.resolveMode = mode;
+    },
+
+    restoreFileLinesState(file) {
+      file.inlineLines.forEach((line) => {
+        if (line.hasConflict || line.isHeader) {
+          line.isSelected = false;
+          line.isUnselected = false;
+        }
+      });
+
+      file.parallelLines.forEach((lines) => {
+        const left = lines[0];
+        const right = lines[1];
+        const isLeftMatch = left.hasConflict || left.isHeader;
+        const isRightMatch = right.hasConflict || right.isHeader;
+
+        if (isLeftMatch || isRightMatch) {
+          left.isSelected = false;
+          left.isUnselected = false;
+          right.isSelected = false;
+          right.isUnselected = false;
+        }
+      });
+    },
+
+    isReadyToCommit() {
+      const files = this.state.conflictsData.files;
+      const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
+      let unresolved = 0;
+
+      for (let i = 0, l = files.length; i < l; i++) {
+        let file = files[i];
+
+        if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
+          let numberConflicts = 0;
+          let resolvedConflicts = Object.keys(file.resolutionData).length
+
+          // We only check for conflicts type 'text'
+          // since conflicts `text_editor` can´t be resolved in interactive mode
+          if (file.type === CONFLICT_TYPES.TEXT) {
+            for (let j = 0, k = file.sections.length; j < k; j++) {
+              if (file.sections[j].conflict) {
+                numberConflicts++;
+              }
+            }
+
+            if (resolvedConflicts !== numberConflicts) {
+              unresolved++;
+            }
+          }
+        } else if (file.resolveMode === EDIT_RESOLVE_MODE) {
+
+          // Unlikely to happen since switching to Edit mode saves content automatically.
+          // Checking anyway in case the save strategy changes in the future
+          if (!file.content) {
+            unresolved++;
+            continue;
+          }
+        }
+      }
+
+      return !this.state.isSubmitting && hasCommitMessage && !unresolved;
+    },
+
+    getCommitButtonText() {
+      const initial = 'Commit conflict resolution';
+      const inProgress = 'Committing...';
+
+      return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
+    },
+
+    getCommitData() {
+      let commitData = {};
+
+      commitData = {
+        commit_message: this.state.conflictsData.commitMessage,
+        files: []
+      };
+
+      this.state.conflictsData.files.forEach((file) => {
+        let addFile;
+
+        addFile = {
+          old_path: file.old_path,
+          new_path: file.new_path
+        };
+
+        if (file.type === CONFLICT_TYPES.TEXT) {
+
+          // Submit only one data for type of editing
+          if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
+            addFile.sections = file.resolutionData;
+          } else if (file.resolveMode === EDIT_RESOLVE_MODE) {
+            addFile.content = file.content;
+          }
+        } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
+          addFile.content = file.content;
+        }
+
+        commitData.files.push(addFile);
+      });
+
+      return commitData;
+    },
+
+    handleSelected(file, sectionId, selection) {
+      Vue.set(file.resolutionData, sectionId, selection);
+
+      file.inlineLines.forEach((line) => {
+        if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
+          this.markLine(line, selection);
+        }
+      });
+
+      file.parallelLines.forEach((lines) => {
+        const left = lines[0];
+        const right = lines[1];
+        const hasSameId = right.id === sectionId || left.id === sectionId;
+        const isLeftMatch = left.hasConflict || left.isHeader;
+        const isRightMatch = right.hasConflict || right.isHeader;
+
+        if (hasSameId && (isLeftMatch || isRightMatch)) {
+          this.markLine(left, selection);
+          this.markLine(right, selection);
+        }
+      });
+    },
+
+    markLine(line, selection) {
+      if (selection === 'head' && line.isHead) {
+        line.isSelected = true;
+        line.isUnselected = false;
+      } else if (selection === 'origin' && line.isOrigin) {
+        line.isSelected = true;
+        line.isUnselected = false;
+      } else {
+        line.isSelected = false;
+        line.isUnselected = true;
+      }
+    },
+
+    setSubmitState(state) {
+      this.state.isSubmitting = state;
+    },
+
+    fileTextTypePresent() {
+      return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
+    }
+  };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..222a5dcfc2e22a884c0634743a5943c77d2efa98
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -0,0 +1,90 @@
+/* eslint-disable */
+//= require vue
+//= require ./merge_conflict_store
+//= require ./merge_conflict_service
+//= require ./mixins/line_conflict_utils
+//= require ./mixins/line_conflict_actions
+//= require ./components/diff_file_editor
+//= require ./components/inline_conflict_lines
+//= require ./components/parallel_conflict_line
+//= require ./components/parallel_conflict_lines
+
+$(() => {
+  const INTERACTIVE_RESOLVE_MODE = 'interactive';
+  const conflictsEl = document.querySelector('#conflicts');
+  const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
+  const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
+    conflictsPath: conflictsEl.dataset.conflictsPath,
+    resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
+  });
+
+  gl.MergeConflictsResolverApp = new Vue({
+    el: '#conflicts',
+    data: mergeConflictsStore.state,
+    components: {
+      'diff-file-editor': gl.mergeConflicts.diffFileEditor,
+      'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
+      'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
+    },
+    computed: {
+      conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
+      readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
+      commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
+      showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
+    },
+    created() {
+      mergeConflictsService
+        .fetchConflictsData()
+        .done((data) => {
+          if (data.type === 'error') {
+            mergeConflictsStore.setFailedRequest(data.message);
+          } else {
+            mergeConflictsStore.setConflictsData(data);
+          }
+        })
+        .error(() => {
+          mergeConflictsStore.setFailedRequest();
+        })
+        .always(() => {
+          mergeConflictsStore.setLoadingState(false);
+
+          this.$nextTick(() => {
+            $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
+          });
+        });
+    },
+    methods: {
+      handleViewTypeChange(viewType) {
+        mergeConflictsStore.setViewType(viewType);
+      },
+      onClickResolveModeButton(file, mode) {
+        if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
+          mergeConflictsStore.setPromptConfirmationState(file, true);
+          return;
+        }
+
+        mergeConflictsStore.setFileResolveMode(file, mode);
+      },
+      acceptDiscardConfirmation(file) {
+        mergeConflictsStore.setPromptConfirmationState(file, false);
+        mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
+      },
+      cancelDiscardConfirmation(file) {
+        mergeConflictsStore.setPromptConfirmationState(file, false);
+      },
+      commit() {
+        mergeConflictsStore.setSubmitState(true);
+
+        mergeConflictsService
+          .submitResolveConflicts(mergeConflictsStore.getCommitData())
+          .done((data) => {
+            window.location.href = data.redirect_to;
+          })
+          .error(() => {
+            mergeConflictsStore.setSubmitState(false);
+            new Flash('Failed to save merge conflicts resolutions. Please try again!');
+          });
+      }
+    }
+  })
+});
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..c8de586aa212720d182badd13ba3ff647f714038
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -0,0 +1,13 @@
+/* eslint-disable */
+((global) => {
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.actions = {
+    methods: {
+      handleSelected(file, sectionId, selection) {
+        gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
+      }
+    }
+  };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..88c3a20ce132b8ba2fa2c6c207b6d06fca51db81
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -0,0 +1,19 @@
+/* eslint-disable */
+((global) => {
+  global.mergeConflicts = global.mergeConflicts || {};
+
+  global.mergeConflicts.utils = {
+    methods: {
+      lineCssClass(line) {
+        return {
+          'head': line.isHead,
+          'origin': line.isOrigin,
+          'match': line.hasMatch,
+          'selected': line.isSelected,
+          'unselected': line.isUnselected
+        };
+      }
+    }
+  };
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 47e6dd1084d2a6a6527d49e0dedc0f46ee58c9c8..d3bd1e846c1cec8fa4d77208052f046091463a57 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,10 +1,7 @@
+/* eslint-disable */
 
 /*= require jquery.waitforimages */
-
-
 /*= require task_list */
-
-
 /*= require merge_request_tabs */
 
 (function() {
@@ -12,6 +9,11 @@
 
   this.MergeRequest = (function() {
     function MergeRequest(opts) {
+      // Initialize MergeRequest behavior
+      //
+      // Options:
+      //   action - String, current controller action
+      //
       this.opts = opts != null ? opts : {};
       this.submitNoteForm = bind(this.submitNoteForm, this);
       this.$el = $('.merge-request');
@@ -21,6 +23,7 @@
         };
       })(this));
       this.initTabs();
+      // Prevent duplicate event bindings
       this.disableTaskList();
       this.initMRBtnListeners();
       if ($("a.btn-close").length) {
@@ -28,16 +31,16 @@
       }
     }
 
+    // Local jQuery finder
     MergeRequest.prototype.$ = function(selector) {
       return this.$el.find(selector);
     };
 
     MergeRequest.prototype.initTabs = function() {
-      if (this.opts.action !== 'new') {
-        return new MergeRequestTabs(this.opts);
-      } else {
-        return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
+      if (window.mrTabs) {
+        window.mrTabs.unbindEvents();
       }
+      window.mrTabs = new MergeRequestTabs(this.opts);
     };
 
     MergeRequest.prototype.showAllCommits = function() {
@@ -94,8 +97,14 @@
       return $.ajax({
         type: 'PATCH',
         url: $('form.js-issuable-update').attr('action'),
-        data: patchData
+        data: patchData,
+        success: function(mergeRequest) {
+          document.querySelector('#task_status').innerText = mergeRequest.task_status;
+          document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
+        }
       });
+    // TODO (rspeicher): Make the merge request description inline-editable like a
+    // note so that we can re-use its form here
     };
 
     return MergeRequest;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 52c2ed6101259ca038913ee0cb46f02a8e1218a2..860ee5df57e02f35aebb37da56367051e969e377 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,50 @@
+/* eslint-disable */
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
+/*= require js.cookie */
 
-/*= require jquery.cookie */
-
+//
+// ### Example Markup
+//
+//   <ul class="nav-links merge-request-tabs">
+//     <li class="notes-tab active">
+//       <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+//         Discussion
+//       </a>
+//     </li>
+//     <li class="commits-tab">
+//       <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+//         Commits
+//       </a>
+//     </li>
+//     <li class="diffs-tab">
+//       <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+//         Diffs
+//       </a>
+//     </li>
+//   </ul>
+//
+//   <div class="tab-content">
+//     <div class="notes tab-pane active" id="notes">
+//       Notes Content
+//     </div>
+//     <div class="commits tab-pane" id="commits">
+//       Commits Content
+//     </div>
+//     <div class="diffs tab-pane" id="diffs">
+//       Diffs Content
+//     </div>
+//   </div>
+//
+//   <div class="mr-loading-status">
+//     <div class="loading">
+//       Loading Animation
+//     </div>
+//   </div>
+//
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -9,21 +53,36 @@
 
     MergeRequestTabs.prototype.buildsLoaded = false;
 
+    MergeRequestTabs.prototype.pipelinesLoaded = false;
+
     MergeRequestTabs.prototype.commitsLoaded = false;
 
+    MergeRequestTabs.prototype.fixedLayoutPref = null;
+
     function MergeRequestTabs(opts) {
       this.opts = opts != null ? opts : {};
+      this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
+
+      this.buildsLoaded = this.opts.buildsLoaded || false;
+
       this.setCurrentAction = bind(this.setCurrentAction, this);
       this.tabShown = bind(this.tabShown, this);
       this.showTab = bind(this.showTab, this);
+      // Store the `location` object, allowing for easier stubbing in tests
       this._location = location;
       this.bindEvents();
       this.activateTab(this.opts.action);
+      this.initAffix();
     }
 
     MergeRequestTabs.prototype.bindEvents = function() {
       $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
-      return $(document).on('click', '.js-show-tab', this.showTab);
+      $(document).on('click', '.js-show-tab', this.showTab);
+    };
+
+    MergeRequestTabs.prototype.unbindEvents = function() {
+      $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
+      $(document).off('click', '.js-show-tab', this.showTab);
     };
 
     MergeRequestTabs.prototype.showTab = function(event) {
@@ -38,11 +97,15 @@
       if (action === 'commits') {
         this.loadCommits($target.attr('href'));
         this.expandView();
-      } else if (action === 'diffs') {
+        this.resetViewContainer();
+      } else if (this.isDiffAction(action)) {
         this.loadDiff($target.attr('href'));
         if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
           this.shrinkView();
         }
+        if (this.diffViewType() === 'parallel') {
+          this.expandViewContainer();
+        }
         navBarHeight = $('.navbar-gitlab').outerHeight();
         $.scrollTo(".merge-request-details .merge-request-tabs", {
           offset: -navBarHeight
@@ -50,10 +113,18 @@
       } else if (action === 'builds') {
         this.loadBuilds($target.attr('href'));
         this.expandView();
+        this.resetViewContainer();
+      } else if (action === 'pipelines') {
+        this.loadPipelines($target.attr('href'));
+        this.expandView();
+        this.resetViewContainer();
       } else {
         this.expandView();
+        this.resetViewContainer();
+      }
+      if (this.opts.setUrl) {
+        this.setCurrentAction(action);
       }
-      return this.setCurrentAction(action);
     };
 
     MergeRequestTabs.prototype.scrollToElement = function(container) {
@@ -69,26 +140,57 @@
       }
     };
 
+    // Activate a tab based on the current action
     MergeRequestTabs.prototype.activateTab = function(action) {
       if (action === 'show') {
         action = 'notes';
       }
-      return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
+      $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
     };
 
+    // Replaces the current Merge Request-specific action in the URL with a new one
+    //
+    // If the action is "notes", the URL is reset to the standard
+    // `MergeRequests#show` route.
+    //
+    // Examples:
+    //
+    //   location.pathname # => "/namespace/project/merge_requests/1"
+    //   setCurrentAction('diffs')
+    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+    //
+    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+    //   setCurrentAction('notes')
+    //   location.pathname # => "/namespace/project/merge_requests/1"
+    //
+    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+    //   setCurrentAction('commits')
+    //   location.pathname # => "/namespace/project/merge_requests/1/commits"
+    //
+    // Returns the new URL String
     MergeRequestTabs.prototype.setCurrentAction = function(action) {
       var new_state;
+      // Normalize action, just to be safe
       if (action === 'show') {
         action = 'notes';
       }
-      new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
+      this.currentAction = action;
+      // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
+      new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
+      // Append the new action if we're on a tab other than 'notes'
       if (action !== 'notes') {
         new_state += "/" + action;
       }
+      // Ensure parameters and hash come along for the ride
       new_state += this._location.search + this._location.hash;
       history.replaceState({
         turbolinks: true,
         url: new_state
+      // Replace the current history state with the new one without breaking
+      // Turbolinks' history.
+      //
+      // See https://github.com/rails/turbolinks/issues/363
       }, document.title, new_state);
       return new_state;
     };
@@ -114,20 +216,33 @@
       if (this.diffsLoaded) {
         return;
       }
+
+      // We extract pathname for the current Changes tab anchor href
+      // some pages like MergeRequestsController#new has query parameters on that anchor
+      var url = gl.utils.parseUrl(source);
+
       return this._get({
-        url: (source + ".json") + this._location.search,
+        url: (url.pathname + ".json") + this._location.search,
         success: (function(_this) {
           return function(data) {
             $('#diffs').html(data.html);
+
+            if (typeof DiffNotesApp !== 'undefined') {
+              DiffNotesApp.compileComponents();
+            }
+
             gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
             $('#diffs .js-syntax-highlight').syntaxHighlight();
             $('#diffs .diff-file').singleFileDiff();
-            if (_this.diffViewType() === 'parallel') {
+            if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
               _this.expandViewContainer();
             }
             _this.diffsLoaded = true;
-            _this.scrollToElement("#diffs");
-            _this.highlighSelectedLine();
+            var anchoredDiff = gl.utils.getLocationHash();
+            if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
+              _this.scrollToElement("#diffs");
+              _this.highlighSelectedLine();
+            });
             _this.filesCommentButton = $('.files .diff-file').filesCommentButton();
             return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
               e.preventDefault();
@@ -140,15 +255,26 @@
       });
     };
 
+    MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
+      var diffTitle = $('#file-path-' + anchoredDiff);
+      var diffFile = diffTitle.closest('.diff-file');
+      var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+      if (nothingHereBlock.length) {
+        diffFile.singleFileDiff(true, cb);
+      } else {
+        cb();
+      }
+    };
+
     MergeRequestTabs.prototype.highlighSelectedLine = function() {
       var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
       $('.hll').removeClass('hll');
       locationHash = window.location.hash;
       if (locationHash !== '') {
-        hashClassString = "." + (locationHash.replace('#', ''));
+        dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
         $diffLine = $(locationHash + ":not(.match)", $('#diffs'));
         if (!$diffLine.is('tr')) {
-          $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString);
+          $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
         } else {
           $diffLine = $diffLine.find('td');
         }
@@ -171,12 +297,31 @@
             document.querySelector("div#builds").innerHTML = data.html;
             gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
             _this.buildsLoaded = true;
+            if (!this.pipelines) this.pipelines = new gl.Pipelines();
             return _this.scrollToElement("#builds");
           };
         })(this)
       });
     };
 
+    MergeRequestTabs.prototype.loadPipelines = function(source) {
+      if (this.pipelinesLoaded) {
+        return;
+      }
+      return this._get({
+        url: source + ".json",
+        success: function(data) {
+          $('#pipelines').html(data.html);
+          gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
+          this.pipelinesLoaded = true;
+          return this.scrollToElement("#pipelines");
+        }.bind(this)
+      });
+    };
+
+    // Show or hide the loading spinner
+    //
+    // status - Boolean, true to show, false to hide
     MergeRequestTabs.prototype.toggleLoading = function(status) {
       return $('.mr-loading-status .loading').toggle(status);
     };
@@ -205,8 +350,23 @@
       return $('.inline-parallel-buttons a.active').data('view-type');
     };
 
+    MergeRequestTabs.prototype.isDiffAction = function(action) {
+      return action === 'diffs' || action === 'new/diffs'
+    };
+
     MergeRequestTabs.prototype.expandViewContainer = function() {
-      return $('.container-fluid').removeClass('container-limited');
+      var $wrapper = $('.content-wrapper .container-fluid');
+      if (this.fixedLayoutPref === null) {
+        this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+      }
+      $wrapper.removeClass('container-limited');
+    };
+
+    MergeRequestTabs.prototype.resetViewContainer = function() {
+      if (this.fixedLayoutPref !== null) {
+        $('.content-wrapper .container-fluid')
+          .toggleClass('container-limited', this.fixedLayoutPref);
+      }
     };
 
     MergeRequestTabs.prototype.shrinkView = function() {
@@ -216,12 +376,14 @@
         if ($gutterIcon.is('.fa-angle-double-right')) {
           return $gutterIcon.closest('a').trigger('click', [true]);
         }
+      // Wait until listeners are set
+      // Only when sidebar is expanded
       }, 0);
     };
 
     MergeRequestTabs.prototype.expandView = function() {
       var $gutterIcon;
-      if ($.cookie('collapsed_gutter') === 'true') {
+      if (Cookies.get('collapsed_gutter') === 'true') {
         return;
       }
       $gutterIcon = $('.js-sidebar-toggle i:visible');
@@ -230,6 +392,46 @@
           return $gutterIcon.closest('a').trigger('click', [true]);
         }
       }, 0);
+    // Expand the issuable sidebar unless the user explicitly collapsed it
+    // Wait until listeners are set
+    // Only when sidebar is collapsed
+    };
+
+    MergeRequestTabs.prototype.initAffix = function () {
+      var $tabs = $('.js-tabs-affix');
+
+      // Screen space on small screens is usually very sparse
+      // So we dont affix the tabs on these
+      if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+      var $diffTabs = $('#diff-notes-app'),
+        $fixedNav = $('.navbar-fixed-top'),
+        $layoutNav = $('.layout-nav');
+
+      $tabs.off('affix.bs.affix affix-top.bs.affix')
+        .affix({
+          offset: {
+            top: function () {
+              var tabsTop = $diffTabs.offset().top - $tabs.height();
+              tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
+
+              return tabsTop;
+            }
+          }
+        }).on('affix.bs.affix', function () {
+          $diffTabs.css({
+            marginTop: $tabs.height()
+          });
+        }).on('affix-top.bs.affix', function () {
+          $diffTabs.css({
+            marginTop: ''
+          });
+        });
+
+      // Fix bug when reloading the page already scrolling
+      if ($tabs.hasClass('affix')) {
+        $tabs.trigger('affix.bs.affix');
+      }
     };
 
     return MergeRequestTabs;
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js.es6
similarity index 59%
rename from app/assets/javascripts/merge_request_widget.js
rename to app/assets/javascripts/merge_request_widget.js.es6
index 362aaa906d033ce110e6c838cc7cbc14fd9d0dfd..56c87af3226163faaed7b17a13494ffd9592c03a 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,20 +1,58 @@
-(function() {
+/* eslint-disable */
+ ((global) => {
   var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
 
-  this.MergeRequestWidget = (function() {
+  const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
+       <div class="ci_widget ci-success">
+         <%= ci_success_icon %>
+         <span>
+           Deployed to
+           <a href="<%- url %>" target="_blank" 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">
+             <i class="fa fa-external-link"></i>
+             View on <%- external_url_formatted %>
+           </a>
+         </span>
+         <span class="stop-env-container js-stop-env-link">
+          <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
+            <i class="fa fa-stop-circle-o"/>
+            Stop environment
+          </a>
+         </span>
+       </div>
+     </div>`;
+
+   global.MergeRequestWidget = (function() {
     function MergeRequestWidget(opts) {
+      // Initialize MergeRequestWidget behavior
+      //
+      //   check_enable           - Boolean, whether to check automerge status
+      //   merge_check_url - String, URL to use to check automerge status
+      //   ci_status_url        - String, URL to use to check CI status
+      //
       this.opts = opts;
+      this.$widgetBody = $('.mr-widget-body');
       $('#modal_merge_info').modal({
         show: false
       });
       this.firstCICheck = true;
       this.readyForCICheck = false;
+      this.readyForCIEnvironmentCheck = false;
       this.cancel = false;
       clearInterval(this.fetchBuildStatusInterval);
+      clearInterval(this.fetchBuildEnvironmentStatusInterval);
       this.clearEventListeners();
       this.addEventListeners();
       this.getCIStatus(false);
+      this.getCIEnvironmentsStatus();
+      this.retrieveSuccessIcon();
       this.pollCIStatus();
+      this.pollCIEnvironmentsStatus();
       notifyPermissions();
     }
 
@@ -28,13 +66,14 @@
 
     MergeRequestWidget.prototype.addEventListeners = function() {
       var allowedPages;
-      allowedPages = ['show', 'commits', 'builds', 'changes'];
+      allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
       return $(document).on('page:change.merge_request', (function(_this) {
         return function() {
           var page;
           page = $('body').data('page').split(':').last();
           if (allowedPages.indexOf(page) < 0) {
             clearInterval(_this.fetchBuildStatusInterval);
+            clearInterval(_this.fetchBuildEnvironmentStatusInterval);
             _this.cancelPolling();
             return _this.clearEventListeners();
           }
@@ -42,6 +81,12 @@
       })(this));
     };
 
+    MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
+       const $ciSuccessIcon = $('.js-success-icon');
+       this.$ciSuccessIcon = $ciSuccessIcon.html();
+       $ciSuccessIcon.remove();
+     }
+
     MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
       if (deleteSourceBranch == null) {
         deleteSourceBranch = false;
@@ -53,10 +98,10 @@
           return function(data) {
             var callback, urlSuffix;
             if (data.state === "merged") {
-              urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
+              urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
               return window.location.href = window.location.pathname + urlSuffix;
             } else if (data.merge_error) {
-              return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
+              return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
             } else {
               callback = function() {
                 return merge_request_widget.mergeInProgress(deleteSourceBranch);
@@ -112,12 +157,15 @@
           if (data.status === '') {
             return;
           }
+          if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
           if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
             _this.opts.ci_status = data.status;
             _this.showCIStatus(data.status);
             if (data.coverage) {
               _this.showCICoverage(data.coverage);
             }
+            // The first check should only update the UI, a notification
+            // should only be displayed on status changes
             if (showNotification && !_this.firstCICheck) {
               status = _this.ciLabelForStatus(data.status);
               if (status === "preparing") {
@@ -142,6 +190,46 @@
       })(this));
     };
 
+    MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
+      this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
+        if (!this.readyForCIEnvironmentCheck) return;
+        this.getCIEnvironmentsStatus();
+        this.readyForCIEnvironmentCheck = false;
+      }, 300000);
+    };
+
+    MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
+      $.getJSON(this.opts.ci_environments_status_url, (environments) => {
+        if (this.cancel) return;
+        this.readyForCIEnvironmentCheck = true;
+        if (environments && environments.length) this.renderEnvironments(environments);
+      });
+    };
+
+    MergeRequestWidget.prototype.renderEnvironments = function(environments) {
+      for (let i = 0; i < environments.length; i++) {
+        const environment = environments[i];
+        if ($(`.mr-state-widget #${ environment.id }`).length) return;
+        const $template = $(DEPLOYMENT_TEMPLATE);
+        if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
+        
+        if (!environment.stop_url) {
+          $('.js-stop-env-link', $template).remove();
+        }
+        
+        if (environment.deployed_at && environment.deployed_at_formatted) {
+          environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.';
+        } else {
+          $('.js-environment-timeago', $template).remove();
+          environment.name += '.';
+        }
+        environment.ci_success_icon = this.$ciSuccessIcon;
+        const templateString = _.unescape($template[0].outerHTML);
+        const template = _.template(templateString)(environment)
+        this.$widgetBody.before(template);
+      }
+     };
+
     MergeRequestWidget.prototype.showCIStatus = function(state) {
       var allowed_states;
       if (state == null) {
@@ -182,4 +270,4 @@
 
   })();
 
-}).call(this);
+ })(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 1fed38661a2d055c6bd0194643826ee9b78f0616..7ad86d8c0845bbc929c71dada94067c7e2656c16 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e8d51da7d5829ae07bf60c1a0f824df5d0e50b3b..9299c96e8ea680275c67cc0707cd7e57998a6092 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Milestone = (function() {
     Milestone.updateIssue = function(li, issue_url, data) {
@@ -110,6 +111,7 @@
         },
         update: function(event, ui) {
           var data;
+          // Prevents sorting from container which element has been removed.
           if ($(this).find(ui.item).length > 0) {
             data = $(this).sortable("serialize");
             return Milestone.sortIssues(data);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index a0b65d20c03425c71954e1b6d3e968f485dd740c..d1cd38ad1104ad4648cb2bf06f2471b87cc9af06 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.MilestoneSelect = (function() {
     function MilestoneSelect(currentProject) {
@@ -7,7 +8,7 @@
         this.currentProject = JSON.parse(currentProject);
       }
       $('.js-milestone-select').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;
+        var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
         $dropdown = $(dropdown);
         projectId = $dropdown.data('project-id');
         milestonesUrl = $dropdown.data('milestones');
@@ -15,6 +16,7 @@
         selectedMilestone = $dropdown.data('selected');
         showNo = $dropdown.data('show-no');
         showAny = $dropdown.data('show-any');
+        showMenuAbove = $dropdown.data('showMenuAbove');
         showUpcoming = $dropdown.data('show-upcoming');
         useId = $dropdown.data('use-id');
         defaultLabel = $dropdown.data('default-label');
@@ -31,12 +33,12 @@
           collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
         }
         return $dropdown.glDropdown({
+          showMenuAbove: showMenuAbove,
           data: function(term, callback) {
             return $.ajax({
               url: milestonesUrl
             }).done(function(data) {
-              var extraOptions;
-              extraOptions = [];
+              var extraOptions = [];
               if (showAny) {
                 extraOptions.push({
                   id: 0,
@@ -58,10 +60,14 @@
                   title: 'Upcoming'
                 });
               }
-              if (extraOptions.length > 2) {
+              if (extraOptions.length) {
                 extraOptions.push('divider');
               }
-              return callback(extraOptions.concat(data));
+
+              callback(extraOptions.concat(data));
+              if (showMenuAbove) {
+                $dropdown.data('glDropdown').positionMenuAbove();
+              }
             });
           },
           filterable: true,
@@ -69,19 +75,20 @@
             fields: ['title']
           },
           selectable: true,
-          toggleLabel: function(selected) {
-            if (selected && 'id' in selected) {
+          toggleLabel: function(selected, el, e) {
+            if (selected && 'id' in selected && $(el).hasClass('is-active')) {
               return selected.title;
             } else {
               return defaultLabel;
             }
           },
+          defaultLabel: defaultLabel,
           fieldName: $dropdown.data('field-name'),
           text: function(milestone) {
             return _.escape(milestone.title);
           },
           id: function(milestone) {
-            if (!useId) {
+            if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
               return milestone.name;
             } else {
               return milestone.id;
@@ -92,17 +99,24 @@
           },
           hidden: function() {
             $selectbox.hide();
+            // display:block overrides the hide-collapse rule
             return $value.css('display', '');
           },
-          clicked: function(selected) {
+          vue: $dropdown.hasClass('js-issue-board-sidebar'),
+          clicked: function(selected, $el, e) {
             var data, isIssueIndex, isMRIndex, page;
             page = $('body').data('page');
             isIssueIndex = page === 'projects:issues:index';
             isMRIndex = (page === page && page === 'projects:merge_requests:index');
-            if ($dropdown.hasClass('js-filter-bulk-update')) {
+            if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+              e.preventDefault();
               return;
             }
-            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+            if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+              gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
+              gl.issueBoards.BoardsStore.updateFiltersUrl();
+              e.preventDefault();
+            } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
               if (selected.name != null) {
                 selectedMilestone = selected.name;
               } else {
@@ -111,6 +125,24 @@
               return Issuable.filterResults($dropdown.closest('form'));
             } else if ($dropdown.hasClass('js-filter-submit')) {
               return $dropdown.closest('form').submit();
+            } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+              if (selected.id !== -1) {
+                Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
+                  id: selected.id,
+                  title: selected.name
+                }));
+              } else {
+                Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
+              }
+
+              $dropdown.trigger('loading.gl.dropdown');
+              $loading.fadeIn();
+
+              gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+                .then(function () {
+                  $dropdown.trigger('loaded.gl.dropdown');
+                  $loading.fadeOut();
+                });
             } else {
               selected = $selectbox.find('input[type="hidden"]').val();
               data = {};
@@ -130,7 +162,7 @@
                 if (data.milestone != null) {
                   data.milestone.namespace = _this.currentProject.namespace;
                   data.milestone.path = _this.currentProject.path;
-                  data.milestone.remaining = $.timefor(data.milestone.due_date);
+                  data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
                   $value.html(milestoneLinkTemplate(data.milestone));
                   return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
                 } else {
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 10f4fd106d855b20bf81ab72ffe289cb2cd43067..d1168227b77be069be13dcc2ed871dc4e8a97ad7 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js
similarity index 96%
rename from app/assets/javascripts/network/branch-graph.js
rename to app/assets/javascripts/network/branch_graph.js
index c0fec1f860773b4cb42a9019307b31a6cbfe4963..74dbeb947417b1c5a279f48d4e6f634f40fb4871 100644
--- a/app/assets/javascripts/network/branch-graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -90,6 +91,7 @@
       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++);
@@ -112,6 +114,7 @@
       for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
         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"
@@ -119,6 +122,7 @@
           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"
@@ -207,6 +211,7 @@
       }
       r = this.r;
       shortrefs = commit.refs;
+      // Truncate if longer than 15 chars
       if (shortrefs.length > 17) {
         shortrefs = shortrefs.substr(0, 15) + "…";
       }
@@ -217,6 +222,7 @@
         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,
@@ -229,6 +235,7 @@
       });
       label = r.set(rect, text);
       label.transform(["t", -rect.getBBox().width - 15, 0]);
+      // Set text to front
       return text.toFront();
     };
 
@@ -283,11 +290,13 @@
         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";
@@ -298,13 +307,17 @@
           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,
@@ -325,6 +338,7 @@
           "fill-opacity": .5,
           stroke: "none"
         });
+        // Displayed in the center
         return this.element.scrollTop(y - this.graphHeight / 2);
       }
     };
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 7baebcd100a9203cd7ccddb3187617c7115c8445..8898e7ace4322647c8017909ff818bef581a38e5 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Network = (function() {
     function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 6a7422a77553bf88e625cc29249aa4b9d2cf0afc..a192273a18060963f4e85258ad9ca77dd94018ad 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,8 +1,16 @@
-
+/* eslint-disable */
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
 /*= require_tree . */
 
 (function() {
   $(function() {
+    if (!$(".network-graph").length) return;
+
     var network_graph;
     network_graph = new Network({
       url: $(".network-graph").attr('data-url'),
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 20aa2fced27e0e8cbff5d853a132d21f7df8286d..0e643b0ff142c6d8097d4eeee75a7458fb4b3a45 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (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++) { if (i in this && this[i] === item) return i; } return -1; };
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 21bf8867f7b566be7680dc92982fb7d2901b78c1..acb529023fa5138d3637f7562a9a958e94c82896 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9ece474d9941ea4c8f64bcfe67d98c2da3993022..4976eef28965a37e647adbba1d1f971bc0801e57 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,22 +1,11 @@
+/* eslint-disable */
 
 /*= require autosave */
-
-
 /*= require autosize */
-
-
 /*= require dropzone */
-
-
 /*= require dropzone_input */
-
-
 /*= require gfm_auto_complete */
-
-
 /*= require jquery.atwho */
-
-
 /*= require task_list */
 
 (function() {
@@ -60,25 +49,43 @@
     }
 
     Notes.prototype.addBinding = function() {
+      // add note to UI after creation
       $(document).on("ajax:success", ".js-main-target-form", this.addNote);
       $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
+      // catch note ajax errors
       $(document).on("ajax:error", ".js-main-target-form", this.addNoteError);
+      // change note in UI after update
       $(document).on("ajax:success", "form.edit-note", this.updateNote);
+      // Edit note link
       $(document).on("click", ".js-note-edit", this.showEditForm);
       $(document).on("click", ".note-edit-cancel", this.cancelEdit);
+      // Reopen and close actions for Issue/MR combined with note form submit
       $(document).on("click", ".js-comment-button", this.updateCloseButton);
       $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+      // resolve a discussion
+      $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
+      // remove a note (in general)
       $(document).on("click", ".js-note-delete", this.removeNote);
+      // delete note attachment
       $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+      // reset main target form after submit
       $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
       $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
+      // reset main target form when clicking discard
       $(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+      // update the file name when an attachment is selected
       $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+      // reply to diff/discussion notes
       $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
+      // add diff note
       $(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
+      // hide diff note form
       $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+      // fetch notes when tab becomes visible
       $(document).on("visibilitychange", this.visibilityChange);
+      // when issue status changes, we need to refresh data
       $(document).on("issuable:change", this.refresh);
+      // when a key is clicked on the notes
       return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
     };
 
@@ -100,6 +107,7 @@
       $(document).off("click", ".js-note-target-close");
       $(document).off("click", ".js-note-discard");
       $(document).off("keydown", ".js-note-text");
+      $(document).off('click', '.js-comment-resolve-button');
       $('.note .js-task-list-container').taskList('disable');
       return $(document).off('tasklist:changed', '.note .js-task-list-container');
     };
@@ -110,6 +118,7 @@
         return;
       }
       $textarea = $(e.target);
+      // Edit previous note when UP arrow is hit
       switch (e.which) {
         case 38:
           if ($textarea.val() !== '') {
@@ -121,6 +130,7 @@
             return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
           }
           break;
+        // Cancel creating diff note or editing any note when ESCAPE is hit
         case 27:
           discussionNoteForm = $textarea.closest('.js-discussion-note-form');
           if (discussionNoteForm.length) {
@@ -201,7 +211,7 @@
     Increase @pollingInterval up to 120 seconds on every function call,
     if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
     will reset to @basePollingInterval.
-    
+
     Note: this function is used to gradually increase the polling interval
     if there aren't new notes coming from the server
      */
@@ -223,7 +233,7 @@
 
     /*
     Render note in main comments area.
-    
+
     Note: for rendering inline notes use renderDiscussionNote
      */
 
@@ -231,7 +241,13 @@
       var $notesList, votesBlock;
       if (!note.valid) {
         if (note.award) {
-          new Flash('You have already awarded this emoji!', 'alert');
+          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();
+          }
         }
         return;
       }
@@ -239,12 +255,16 @@
         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)) {
         this.note_ids.push(note.id);
         $notesList = $('ul.main-notes-list');
         $notesList.append(note.html).syntaxHighlight();
+        // Update datetime format on the recent note
         gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
         this.initTaskList();
+        this.refresh();
         return this.updateNotesCount(1);
       }
     };
@@ -265,7 +285,7 @@
 
     /*
     Render note in discussion area.
-    
+
     Note: for rendering inline notes use renderDiscussionNote
      */
 
@@ -282,21 +302,33 @@
       row = form.closest("tr");
       note_html = $(note.html);
       note_html.syntaxHighlight();
+      // is this the first note of discussion?
       discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
       if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
         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();
+        // Before that, the container didn't exist
         discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+        // Add note to 'Changes' page discussions
         discussionContainer.append(note_html);
+        // Init discussion on 'Discussion' page if it is merge request page
         if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
           $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
         }
       } else {
+        // append new note to all matching discussions
         discussionContainer.append(note_html);
       }
+
+      if (typeof DiffNotesApp !== 'undefined') {
+        DiffNotesApp.compileComponents();
+      }
+
       gl.utils.localTimeAgo($('.js-timeago', note_html), false);
       return this.updateNotesCount(1);
     };
@@ -304,7 +336,7 @@
 
     /*
     Called in response the main target form has been successfully submitted.
-    
+
     Removes any errors.
     Resets text and preview.
     Resets buttons.
@@ -313,11 +345,18 @@
     Notes.prototype.resetMainTargetForm = function(e) {
       var form;
       form = $(".js-main-target-form");
+      // remove validation errors
       form.find(".js-errors").remove();
+      // reset text and preview
       form.find(".js-md-write-button").click();
       form.find(".js-note-text").val("").trigger("input");
       form.find(".js-note-text").data("autosave").reset();
-      return this.updateTargetButtons(e);
+
+      var event = document.createEvent('Event');
+      event.initEvent('autosize:update', true, false);
+      form.find('.js-autosize')[0].dispatchEvent(event);
+
+      this.updateTargetButtons(e);
     };
 
     Notes.prototype.reenableTargetFormSubmitButton = function() {
@@ -329,27 +368,32 @@
 
     /*
     Shows the main form and does some setup on it.
-    
+
     Sets some hidden fields in the form.
      */
 
     Notes.prototype.setupMainTargetNoteForm = function() {
       var form;
+      // find the form
       form = $(".js-new-note-form");
+      // Set a global clone of the form for later cloning
       this.formClone = form.clone();
+      // show the form
       this.setupNoteForm(form);
+      // fix classes
       form.removeClass("js-new-note-form");
       form.addClass("js-main-target-form");
       form.find("#note_line_code").remove();
       form.find("#note_position").remove();
       form.find("#note_type").remove();
+      form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
       return this.parentTimeline = form.parents('.timeline');
     };
 
 
     /*
     General note form setup.
-    
+
     deactivates the submit button when text is empty
     hides the preview button when text is empty
     setup GFM auto complete
@@ -366,7 +410,7 @@
 
     /*
     Called in response to the new note form being submitted
-    
+
     Adds new note to list.
      */
 
@@ -381,36 +425,56 @@
 
     /*
     Called in response to the new note form being submitted
-    
+
     Adds new note to list.
      */
 
     Notes.prototype.addDiscussionNote = function(xhr, note, status) {
+      var $form = $(xhr.target);
+
+      if ($form.attr('data-resolve-all') != null) {
+        var projectPath = $form.data('project-path')
+            discussionId = $form.data('discussion-id'),
+            mergeRequestId = $form.data('noteable-iid');
+
+        if (ResolveService != null) {
+          ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
+        }
+      }
+
       this.renderDiscussionNote(note);
-      return this.removeDiscussionNoteForm($(xhr.target));
+      // cleanup after successfully creating a diff/discussion note
+      this.removeDiscussionNoteForm($form);
     };
 
 
     /*
     Called in response to the edit note form being submitted
-    
+
     Updates the current note field.
      */
 
     Notes.prototype.updateNote = function(_xhr, note, _status) {
       var $html, $note_li;
+      // Convert returned HTML to a jQuery object so we can modify it further
       $html = $(note.html);
       gl.utils.localTimeAgo($('.js-timeago', $html));
       $html.syntaxHighlight();
       $html.find('.js-task-list-container').taskList('enable');
+      // Find the note's `li` element by ID and replace it with the updated HTML
       $note_li = $('.note-row-' + note.id);
-      return $note_li.replaceWith($html);
+
+      $note_li.replaceWith($html);
+
+      if (typeof DiffNotesApp !== 'undefined') {
+        DiffNotesApp.compileComponents();
+      }
     };
 
 
     /*
     Called in response to clicking the edit note link
-    
+
     Replaces the note text with the note edit form
     Adds a data attribute to the form with the original content of the note for cancellations
      */
@@ -422,15 +486,20 @@
       note.addClass("is-editting");
       form = note.find(".note-edit-form");
       form.addClass('current-note-edit-form');
+      // Show the attachment delete link
       note.find(".js-note-attachment-delete").show();
       done = function($noteText) {
         var noteTextVal;
+        // Neat little trick to put the cursor at the end
         noteTextVal = $noteText.val();
+        // Store the original note text in a data attribute to retrieve if a user cancels edit.
         form.find('form.edit-note').data('original-note', noteTextVal);
         return $noteText.val('').val(noteTextVal);
       };
       new GLForm(form);
       if ((scrollTo != null) && (myLastNote != null)) {
+        // scroll to the bottom
+        // so the open of the last element doesn't make a jump
         $('html, body').scrollTop($(document).height());
         return $('html, body').animate({
           scrollTop: myLastNote.offset().top - 150
@@ -450,7 +519,7 @@
 
     /*
     Called in response to clicking the edit note link
-    
+
     Hides edit form and restores the original note text to the editor textarea.
      */
 
@@ -466,13 +535,14 @@
       form = note.find(".current-note-edit-form");
       note.removeClass("is-editting");
       form.removeClass("current-note-edit-form");
+      // Replace markdown textarea text with original note text.
       return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
     };
 
 
     /*
     Called in response to deleting a note of any kind.
-    
+
     Removes the actual note from view.
     Removes the whole discussion if the last note is being removed.
      */
@@ -481,24 +551,40 @@
       var noteId;
       noteId = $(e.currentTarget).closest(".note").attr("id");
       $(".note[id='" + noteId + "']").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.
         return function(i, el) {
           var note, notes;
           note = $(el);
           notes = note.closest(".notes");
+
+          if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
+            ref = DiffNotesApp.$refs[noteId];
+
+            if (ref) {
+              ref.$destroy(true);
+            }
+          }
+
+          // check if this is the last note for this line
           if (notes.find(".note").length === 1) {
+            // "Discussions" tab
             notes.closest(".timeline-entry").remove();
+            // "Changes" tab / commit view
             notes.closest("tr").remove();
           }
           return note.remove();
         };
       })(this));
+      // Decrement the "Discussions" counter only once
       return this.updateNotesCount(-1);
     };
 
 
     /*
     Called in response to clicking the delete attachment link
-    
+
     Removes the attachment wrapper view, including image tag if it exists
     Resets the note editing form
      */
@@ -515,7 +601,7 @@
 
     /*
     Called when clicking on the "reply" button for a diff line.
-    
+
     Shows the note form below the notes.
      */
 
@@ -523,22 +609,27 @@
       var form, replyLink;
       form = this.formClone.clone();
       replyLink = $(e.target).closest(".js-discussion-reply-button");
-      replyLink.hide();
-      replyLink.after(form);
+      // insert the form after the button
+      replyLink
+        .closest('.discussion-reply-holder')
+        .hide()
+        .after(form);
+      // show the form
       return this.setupDiscussionNoteForm(replyLink, form);
     };
 
 
     /*
     Shows the diff or discussion form and does some setup on it.
-    
+
     Sets some hidden fields in the form.
-    
+
     Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
     and "noteableId" data attributes set.
      */
 
     Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
+      // setup note target
       form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
       form.attr("data-line-code", dataHolder.data("lineCode"));
       form.find("#note_type").val(dataHolder.data("noteType"));
@@ -549,15 +640,29 @@
       form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
       form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
       form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
+      form.find('.js-note-target-close').remove();
       this.setupNoteForm(form);
+
+      if (typeof DiffNotesApp !== 'undefined') {
+        var $commentBtn = form.find('comment-and-resolve-btn');
+        $commentBtn
+          .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
+        DiffNotesApp.$compile($commentBtn.get(0));
+      }
+
       form.find(".js-note-text").focus();
-      return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form");
+      form
+        .find('.js-comment-resolve-button')
+        .attr('data-discussion-id', dataHolder.data('discussionId'));
+      form
+        .removeClass('js-main-target-form')
+        .addClass("discussion-form js-discussion-note-form");
     };
 
 
     /*
     Called when clicking on the "add a comment" button on the side of a diff line.
-    
+
     Inserts a temporary row for the form below the line.
     Sets up the form and shows it.
      */
@@ -570,21 +675,26 @@
       nextRow = row.next();
       hasNotes = nextRow.is(".notes_holder");
       addForm = false;
-      targetContent = ".notes_content";
-      rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>";
+      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>";
+      // In parallel view, look inside the correct left/right pane
       if (this.isParallelView()) {
         lineType = $link.data("lineType");
-        targetContent += "." + lineType;
-        rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>";
+        notesContentSelector += "." + lineType;
+        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(targetContent);
+        nextRow.show();
+        notesContent = nextRow.find(notesContentSelector);
         if (notesContent.length) {
+          notesContent.show();
           replyButton = notesContent.find(".js-discussion-reply-button:visible");
           if (replyButton.length) {
             e.target = replyButton[0];
             $.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
           } else {
+            // In parallel view, the form may not be present in one of the panes
             noteForm = notesContent.find(".js-discussion-note-form");
             if (noteForm.length === 0) {
               addForm = true;
@@ -592,12 +702,16 @@
           }
         }
       } else {
+        // add a notes row and insert the form
         row.after(rowCssToAdd);
+        nextRow = row.next();
+        notesContent = nextRow.find(notesContentSelector);
         addForm = true;
       }
       if (addForm) {
         newForm = this.formClone.clone();
-        newForm.appendTo(row.next().find(targetContent));
+        newForm.appendTo(notesContent);
+        // show the form
         return this.setupDiscussionNoteForm($link, newForm);
       }
     };
@@ -605,7 +719,7 @@
 
     /*
     Called in response to "cancel" on a diff note form.
-    
+
     Shows the reply button again.
     Removes the form and if necessary it's temporary row.
      */
@@ -616,10 +730,15 @@
       glForm = form.data('gl-form');
       glForm.destroy();
       form.find(".js-note-text").data("autosave").reset();
-      form.prev(".js-discussion-reply-button").show();
+      // show the reply button (will only work for replies)
+      form
+        .prev('.discussion-reply-holder')
+        .show();
       if (row.is(".js-temp-notes-holder")) {
+        // remove temporary row for diff lines
         return row.remove();
       } else {
+        // only remove the form
         return form.remove();
       }
     };
@@ -634,13 +753,14 @@
 
     /*
     Called after an attachment file has been selected.
-    
+
     Updates the file name for the selected attachment.
      */
 
     Notes.prototype.updateFormAttachment = function() {
       var filename, form;
       form = $(this).closest("form");
+      // get only the basename
       filename = $(this).val().replace(/^.*[\\\/]/, "");
       return form.find(".js-attachment-filename").text(filename);
     };
@@ -725,6 +845,17 @@
       return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
     };
 
+    Notes.prototype.resolveDiscussion = function () {
+      var $this = $(this),
+          discussionId = $this.attr('data-discussion-id');
+
+      $this
+        .closest('form')
+        .attr('data-discussion-id', discussionId)
+        .attr('data-resolve-all', 'true')
+        .attr('data-project-path', $this.attr('data-project-path'));
+    };
+
     return Notes;
 
   })();
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index a41e9d3fabecd223247fb7b6b57d04d30642d797..ef3f2c6ae73e50df75acab3b775eec1e13f31b87 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.NotificationsDropdown = (function() {
     function NotificationsDropdown() {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 6b2ef17ef6bcb0f781c293b8c0e545fa5918839b..6fbec8efe9b4911d7044773a84138158123491a5 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index b81ed50cb48042c2e86e40648758498c06e4d292..2e4dc62273e9c3bdb6c5309f5b17842511f7cc32 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Pager = {
     init: function(limit, preload, disable, callback) {
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..e6fada5c84c62b376a5c978b484bce73e2e90878
--- /dev/null
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -0,0 +1,44 @@
+/* eslint-disable */
+((global) => {
+
+  class Pipelines {
+    constructor() {
+      this.initGraphToggle();
+      this.addMarginToBuildColumns();
+    }
+
+    initGraphToggle() {
+      this.pipelineGraph = document.querySelector('.pipeline-graph');
+      this.toggleButton = document.querySelector('.toggle-pipeline-btn');
+      this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
+      this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
+    }
+
+    toggleGraph() {
+      const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
+      this.toggleButton.classList.toggle('graph-collapsed');
+      this.pipelineGraph.classList.toggle('graph-collapsed');
+      this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
+    }
+
+    addMarginToBuildColumns() {
+      const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+      for (buildNodeIndex in secondChildBuildNodes) {
+        const buildNode = secondChildBuildNodes[buildNodeIndex];
+        const firstChildBuildNode = buildNode.previousElementSibling;
+        if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
+        const multiBuildColumn = buildNode.closest('.stage-column');
+        const previousColumn = multiBuildColumn.previousElementSibling;
+        if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
+        multiBuildColumn.classList.add('left-margin');
+        firstChildBuildNode.classList.add('left-connector');
+        const columnBuilds = previousColumn.querySelectorAll('.build');
+        if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
+      }
+      this.pipelineGraph.classList.remove('hidden');
+    }
+  }
+
+  global.Pipelines = Pipelines;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 5fd7579964024c842915af9f237863d7335394f0..f2a45a18bedea9e32b207f09246ba56a7f3aa322 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,9 +1,16 @@
+/* eslint-disable */
+// MarkdownPreview
+//
+// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
+// and showing a warning when more than `x` users are referenced.
+//
 (function() {
   var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
 
   this.MarkdownPreview = (function() {
     function MarkdownPreview() {}
 
+    // Minimum number of users referenced before triggering a warning
     MarkdownPreview.prototype.referenceThreshold = 10;
 
     MarkdownPreview.prototype.ajaxCache = {};
@@ -101,8 +108,10 @@
       return;
     }
     lastTextareaPreviewed = $form.find('textarea.markdown-area');
+    // toggle tabs
     $form.find(writeButtonSelector).parent().removeClass('active');
     $form.find(previewButtonSelector).parent().addClass('active');
+    // toggle content
     $form.find('.md-write-holder').hide();
     $form.find('.md-preview-holder').show();
     return markdownPreview.showPreview($form);
@@ -113,8 +122,10 @@
       return;
     }
     lastTextareaPreviewed = null;
+    // toggle tabs
     $form.find(writeButtonSelector).parent().addClass('active');
     $form.find(previewButtonSelector).parent().removeClass('active');
+    // toggle content
     $form.find('.md-write-holder').show();
     $form.find('textarea.markdown-area').focus();
     return $form.find('.md-preview-holder').hide();
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js.es6
similarity index 57%
rename from app/assets/javascripts/profile/gl_crop.js
rename to app/assets/javascripts/profile/gl_crop.js.es6
index a3eea316f67c7aca652b9446ff597734c5243d35..6da6c1d02954d2d6ca9484d74bb20c90e40f79b9 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,39 +1,46 @@
-(function() {
-  var GitLabCrop,
-    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+/* eslint-disable */
+((global) => {
 
-  GitLabCrop = (function() {
-    var FILENAMEREGEX;
+  // Matches everything but the file name
+  const FILENAMEREGEX = /^.*[\\\/]/;
 
-    FILENAMEREGEX = /^.*[\\\/]/;
+  class GitLabCrop {
+    constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
+        exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
 
-    function GitLabCrop(input, opts) {
-      var ref, ref1, ref2, ref3, ref4;
-      if (opts == null) {
-        opts = {};
-      }
-      this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
-      this.onModalHide = bind(this.onModalHide, this);
-      this.onModalShow = bind(this.onModalShow, this);
-      this.onPickImageClick = bind(this.onPickImageClick, this);
+      this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
+      this.onModalHide = this.onModalHide.bind(this);
+      this.onModalShow = this.onModalShow.bind(this);
+      this.onPickImageClick = this.onPickImageClick.bind(this);
       this.fileInput = $(input);
-      this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
-      this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
-      this.filename = this.getElement(this.filename);
-      this.previewImage = this.getElement(this.previewImage);
-      this.pickImageEl = this.getElement(this.pickImageEl);
-      this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
-      this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
       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.exportWidth = exportWidth;
+      this.exportHeight = exportHeight;
+      this.cropBoxWidth = cropBoxWidth;
+      this.cropBoxHeight = cropBoxHeight;
+      this.form = this.fileInput.parents('form');
+      this.filename = filename;
+      this.previewImage = previewImage;
+      this.modalCrop = modalCrop;
+      this.pickImageEl = pickImageEl;
+      this.uploadImageBtn = uploadImageBtn;
+      this.modalCropImg = modalCropImg;
+      this.filename = this.getElement(filename);
+      this.previewImage = this.getElement(previewImage);
+      this.pickImageEl = this.getElement(pickImageEl);
+      this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
+      this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
+      this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
       this.cropActionsBtn = this.modalCrop.find('[data-method]');
       this.bindEvents();
     }
 
-    GitLabCrop.prototype.getElement = function(selector) {
+    getElement(selector) {
       return $(selector, this.form);
-    };
+    }
 
-    GitLabCrop.prototype.bindEvents = function() {
+    bindEvents() {
       var _this;
       _this = this;
       this.fileInput.on('change', function(e) {
@@ -49,13 +56,13 @@
         return _this.onActionBtnClick(btn);
       });
       return this.croppedImageBlob = null;
-    };
+    }
 
-    GitLabCrop.prototype.onPickImageClick = function() {
+    onPickImageClick() {
       return this.fileInput.trigger('click');
-    };
+    }
 
-    GitLabCrop.prototype.onModalShow = function() {
+    onModalShow() {
       var _this;
       _this = this;
       return this.modalCropImg.cropper({
@@ -87,44 +94,44 @@
           });
         }
       });
-    };
+    }
 
-    GitLabCrop.prototype.onModalHide = function() {
+    onModalHide() {
       return this.modalCropImg.attr('src', '').cropper('destroy');
-    };
+    }
 
-    GitLabCrop.prototype.onUploadImageBtnClick = function(e) {
+    onUploadImageBtnClick(e) {
       e.preventDefault();
       this.setBlob();
       this.setPreview();
       this.modalCrop.modal('hide');
       return this.fileInput.val('');
-    };
+    }
 
-    GitLabCrop.prototype.onActionBtnClick = function(btn) {
+    onActionBtnClick(btn) {
       var data, result;
       data = $(btn).data();
       if (this.modalCropImg.data('cropper') && data.method) {
         return result = this.modalCropImg.cropper(data.method, data.option);
       }
-    };
+    }
 
-    GitLabCrop.prototype.onFileInputChange = function(e, input) {
+    onFileInputChange(e, input) {
       return this.readFile(input);
-    };
+    }
 
-    GitLabCrop.prototype.readFile = function(input) {
+    readFile(input) {
       var _this, reader;
       _this = this;
       reader = new FileReader;
-      reader.onload = function() {
+      reader.onload = () => {
         _this.modalCropImg.attr('src', reader.result);
         return _this.modalCrop.modal('show');
       };
       return reader.readAsDataURL(input.files[0]);
-    };
+    }
 
-    GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
+    dataURLtoBlob(dataURL) {
       var array, binary, i, k, len, v;
       binary = atob(dataURL.split(',')[1]);
       array = [];
@@ -135,35 +142,32 @@
       return new Blob([new Uint8Array(array)], {
         type: 'image/png'
       });
-    };
+    }
 
-    GitLabCrop.prototype.setPreview = function() {
+    setPreview() {
       var filename;
       this.previewImage.attr('src', this.dataURL);
       filename = this.fileInput.val().replace(FILENAMEREGEX, '');
       return this.filename.text(filename);
-    };
+    }
 
-    GitLabCrop.prototype.setBlob = function() {
+    setBlob() {
       this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
         width: 200,
         height: 200
       }).toDataURL('image/png');
       return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
-    };
+    }
 
-    GitLabCrop.prototype.getBlob = function() {
+    getBlob() {
       return this.croppedImageBlob;
-    };
-
-    return GitLabCrop;
-
-  })();
+    }
+  }
 
   $.fn.glCrop = function(opts) {
     return this.each(function() {
       return $(this).data('glcrop', new GitLabCrop(this, opts));
     });
-  };
+  }
 
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
deleted file mode 100644
index ed1d87abafed50b7f42e28d07cbf227b456c8e39..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/profile/profile.js
+++ /dev/null
@@ -1,102 +0,0 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
-  this.Profile = (function() {
-    function Profile(opts) {
-      var cropOpts, ref;
-      if (opts == null) {
-        opts = {};
-      }
-      this.onSubmitForm = bind(this.onSubmitForm, this);
-      this.form = (ref = opts.form) != null ? ref : $('.edit-user');
-      $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
-        return $(this).parents('form').submit();
-      });
-      $('#user_notification_email').on('change', function() {
-        return $(this).parents('form').submit();
-      });
-      $('.update-username').on('ajax:before', function() {
-        $('.loading-username').show();
-        $(this).find('.update-success').hide();
-        return $(this).find('.update-failed').hide();
-      });
-      $('.update-username').on('ajax:complete', function() {
-        $('.loading-username').hide();
-        $(this).find('.btn-save').enable();
-        return $(this).find('.loading-gif').hide();
-      });
-      $('.update-notifications').on('ajax:success', function(e, data) {
-        if (data.saved) {
-          return new Flash("Notification settings saved", "notice");
-        } else {
-          return new Flash("Failed to save new settings", "alert");
-        }
-      });
-      this.bindEvents();
-      cropOpts = {
-        filename: '.js-avatar-filename',
-        previewImage: '.avatar-image .avatar',
-        modalCrop: '.modal-profile-crop',
-        pickImageEl: '.js-choose-user-avatar-button',
-        uploadImageBtn: '.js-upload-user-avatar',
-        modalCropImg: '.modal-profile-crop-image'
-      };
-      this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
-    }
-
-    Profile.prototype.bindEvents = function() {
-      return this.form.on('submit', this.onSubmitForm);
-    };
-
-    Profile.prototype.onSubmitForm = function(e) {
-      e.preventDefault();
-      return this.saveForm();
-    };
-
-    Profile.prototype.saveForm = function() {
-      var avatarBlob, formData, self;
-      self = this;
-      formData = new FormData(this.form[0]);
-      avatarBlob = this.avatarGlCrop.getBlob();
-      if (avatarBlob != null) {
-        formData.append('user[avatar]', avatarBlob, 'avatar.png');
-      }
-      return $.ajax({
-        url: this.form.attr('action'),
-        type: this.form.attr('method'),
-        data: formData,
-        dataType: "json",
-        processData: false,
-        contentType: false,
-        success: function(response) {
-          return new Flash(response.message, 'notice');
-        },
-        error: function(jqXHR) {
-          return new Flash(jqXHR.responseJSON.message, 'alert');
-        },
-        complete: function() {
-          window.scrollTo(0, 0);
-          return self.form.find(':input[disabled]').enable();
-        }
-      });
-    };
-
-    return Profile;
-
-  })();
-
-  $(function() {
-    $(document).on('focusout.ssh_key', '#key_key', function() {
-      var $title, comment;
-      $title = $('#key_title');
-      comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
-      if (comment && comment.length > 1 && $title.val() === '') {
-        return $title.val(comment[1]).change();
-      }
-    });
-    if (gl.utils.getPagePath() === 'profiles') {
-      return new Profile();
-    }
-  });
-
-}).call(this);
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..7385838826116c1102f9821d43c574b20289d2f0
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -0,0 +1,101 @@
+/* eslint-disable */
+((global) => {
+
+  class Profile {
+    constructor({ form } = {}) {
+      this.onSubmitForm = this.onSubmitForm.bind(this);
+      this.form = form || $('.edit-user');
+      this.bindEvents();
+      this.initAvatarGlCrop();
+    }
+
+    initAvatarGlCrop() {
+      const cropOpts = {
+        filename: '.js-avatar-filename',
+        previewImage: '.avatar-image .avatar',
+        modalCrop: '.modal-profile-crop',
+        pickImageEl: '.js-choose-user-avatar-button',
+        uploadImageBtn: '.js-upload-user-avatar',
+        modalCropImg: '.modal-profile-crop-image'
+      };
+      this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+    }
+
+    bindEvents() {
+      $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+      $('#user_notification_email').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);
+      this.form.on('submit', this.onSubmitForm);
+    }
+
+    submitForm() {
+      return $(this).parents('form').submit();
+    }
+
+    onSubmitForm(e) {
+      e.preventDefault();
+      return this.saveForm();
+    }
+
+    beforeUpdateUsername() {
+      $('.loading-username').show();
+      $(this).find('.update-success').hide();
+      return $(this).find('.update-failed').hide();
+    }
+
+    afterUpdateUsername() {
+      $('.loading-username').hide();
+      $(this).find('.btn-save').enable();
+      return $(this).find('.loading-gif').hide();
+    }
+
+    onUpdateNotifs(e, data) {
+      return data.saved ?
+        new Flash("Notification settings saved", "notice") :
+        new Flash("Failed to save new settings", "alert");
+    }
+
+    saveForm() {
+      const self = this;
+      const formData = new FormData(this.form[0]);
+      const avatarBlob = this.avatarGlCrop.getBlob();
+
+      if (avatarBlob != null) {
+        formData.append('user[avatar]', avatarBlob, 'avatar.png');
+      }
+
+      return $.ajax({
+        url: this.form.attr('action'),
+        type: this.form.attr('method'),
+        data: formData,
+        dataType: "json",
+        processData: false,
+        contentType: false,
+        success: response => new Flash(response.message, 'notice'),
+        error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
+        complete: () => {
+          window.scrollTo(0, 0);
+          // Enable submit button after requests ends
+          return self.form.find(':input[disabled]').enable();
+        }
+      });
+    }
+  }
+
+  $(function() {
+    $(document).on('focusout.ssh_key', '#key_key', function() {
+      const $title = $('#key_title');
+      const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+      if (comment && comment.length > 1 && $title.val() === '') {
+        return $title.val(comment[1]).change();
+      }
+    // Extract the SSH Key title from its comment
+    });
+    if (global.utils.getPagePath() === 'profiles') {
+      return new Profile();
+    }
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index b95faadc8e72f17e7cdb90eab9203622916bee02..22bee0f61876c3380ae9fc835fc2ee1fc5cce215 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,7 +1,7 @@
+/* eslint-disable */
 
 /*= require_tree . */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index b97f6d2271599bcf85fe9471c7d17bc0a95f934b..2d0c6b166997ee22808f7a4cf3c5cc890598c0ac 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Project = (function() {
     function Project() {
@@ -11,26 +12,24 @@
         url = $("#project_clone").val();
         $('#project_clone').val(url);
         return $('.clone').text(url);
+      // Git protocol switcher
+      // Remove the active class for all buttons (ssh, http, kerberos if shown)
+      // Add the active class for the clicked button
+      // Update the input field
+      // Update the command line instructions
       });
+      // Ref switcher
       this.initRefSwitcher();
       $('.project-refs-select').on('change', function() {
         return $(this).parents('form').submit();
       });
       $('.hide-no-ssh-message').on('click', function(e) {
-        var path;
-        path = '/';
-        $.cookie('hide_no_ssh_message', 'false', {
-          path: path
-        });
+        Cookies.set('hide_no_ssh_message', 'false');
         $(this).parents('.no-ssh-key-message').remove();
         return e.preventDefault();
       });
       $('.hide-no-password-message').on('click', function(e) {
-        var path;
-        path = '/';
-        $.cookie('hide_no_password_message', 'false', {
-          path: path
-        });
+        Cookies.set('hide_no_password_message', 'false');
         $(this).parents('.no-password-message').remove();
         return e.preventDefault();
       });
@@ -65,7 +64,8 @@
               url: $dropdown.data('refs-url'),
               data: {
                 ref: $dropdown.data('ref')
-              }
+              },
+              dataType: "json"
             }).done(function(refs) {
               return callback(refs);
             });
@@ -73,13 +73,13 @@
           selectable: true,
           filterable: true,
           filterByText: true,
-          fieldName: 'ref',
+          fieldName: $dropdown.data('field-name'),
           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));
+              link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
               return $('<li />').append(link);
             }
           },
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 277e71523d5bae41bcab2ce42d5949dfe0023520..61877c6616d872dc85889e890870234eddeda9b1 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectAvatar = (function() {
     function ProjectAvatar() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 4925f0519f069117b0bc44c28e042c003d926a26..ddac5ed83e1fb085114e9081b58f3f2aa03332ba 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -13,8 +14,11 @@
       this.selectRowUp = bind(this.selectRowUp, this);
       this.filePaths = {};
       this.inputElement = this.element.find(".file-finder-input");
+      // init event
       this.initEvent();
+      // focus text input box
       this.inputElement.focus();
+      // load file list
       this.load(this.options.url);
     }
 
@@ -33,15 +37,6 @@
           }
         };
       })(this));
-      return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
-        var path;
-        if (event.target.nodeName !== "A") {
-          path = this.element.find(".tree-item-file-name a", this).attr("href");
-          if (path) {
-            return location.href = path;
-          }
-        }
-      });
     };
 
     ProjectFindFile.prototype.findFile = function() {
@@ -49,8 +44,10 @@
       searchText = this.inputElement.val();
       result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
       return this.renderList(result, searchText);
+    // find file
     };
 
+    // files pathes load
     ProjectFindFile.prototype.load = function(url) {
       return $.ajax({
         url: url,
@@ -67,6 +64,7 @@
       });
     };
 
+    // render result
     ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
       var blobItemUrl, filePath, html, i, j, len, matches, results;
       this.element.find(".tree-table > tbody").empty();
@@ -86,6 +84,7 @@
       return results;
     };
 
+    // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
     highlighter = function(element, text, matches) {
       var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
       lastIndex = 0;
@@ -110,13 +109,15 @@
       return element.append(document.createTextNode(text.substring(lastIndex)));
     };
 
+    // make tbody row html
     ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
       var $tr;
-      $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
+      $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
       if (matches) {
         $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
       } else {
-        $tr.find("a").attr("href", blobItemUrl).text(filePath);
+        $tr.find("a").attr("href", blobItemUrl);
+        $tr.find(".str-truncated").text(filePath);
       }
       return $tr;
     };
@@ -156,10 +157,10 @@
     };
 
     ProjectFindFile.prototype.goToBlob = function() {
-      var path;
-      path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
-      if (path) {
-        return location.href = path;
+      var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
+
+      if ($link.length) {
+        $link.get(0).click();
       }
     };
 
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index d2261c51f35701beb0f62cfda20f9bda2d965a98..fd95f8f2c193b1bf5902cb0d468c38f01133eea0 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectFork = (function() {
     function ProjectFork() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index c61b0cf2fde1d838fe9c43f27461ea0dd6c0064d..f1c4a9fe542e3d42fa5458def75f42cda7e05aa2 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectImport = (function() {
     function ProjectImport() {
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
deleted file mode 100644
index f6a796b325aba297ab0312047e7c6cfce5459c3d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/project_members.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(function() {
-  this.ProjectMembers = (function() {
-    function ProjectMembers() {
-      $('li.project_member').bind('ajax:success', function() {
-        return $(this).fadeOut();
-      });
-    }
-
-    return ProjectMembers;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 798f15e40a05b7cb2765d430bb6dfce43878e0a2..0d3fb31a9cffa9003ecd53013038309660dc75bb 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,9 +1,13 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
   this.ProjectNew = (function() {
     function ProjectNew() {
       this.toggleSettings = bind(this.toggleSettings, this);
+      this.$selects = $('.features select');
+      this.$repoSelects = this.$selects.filter('.js-repo-select');
+
       $('.project-edit-container').on('ajax:before', (function(_this) {
         return function() {
           $('.project-edit-container').hide();
@@ -12,27 +16,77 @@
       })(this));
       this.toggleSettings();
       this.toggleSettingsOnclick();
+      this.toggleRepoVisibility();
     }
 
     ProjectNew.prototype.toggleSettings = function() {
-      this._showOrHide('#project_builds_enabled', '.builds-feature');
-      return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature');
+      var self = this;
+
+      this.$selects.each(function () {
+        var $select = $(this),
+            className = $select.data('field').replace(/_/g, '-')
+              .replace('access-level', 'feature');
+        self._showOrHide($select, '.' + className);
+      });
     };
 
     ProjectNew.prototype.toggleSettingsOnclick = function() {
-      return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings);
+      this.$selects.on('change', this.toggleSettings);
     };
 
     ProjectNew.prototype._showOrHide = function(checkElement, container) {
-      var $container;
-      $container = $(container);
-      if ($(checkElement).prop('checked')) {
+      var $container = $(container);
+
+      if ($(checkElement).val() !== '0') {
         return $container.show();
       } else {
         return $container.hide();
       }
     };
 
+    ProjectNew.prototype.toggleRepoVisibility = function () {
+      var $repoAccessLevel = $('.js-repo-access-level select'),
+          containerRegistry = document.querySelectorAll('.js-container-registry')[0],
+          containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+
+      this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
+        .nextAll()
+        .hide();
+
+      $repoAccessLevel.off('change')
+        .on('change', function () {
+          var selectedVal = parseInt($repoAccessLevel.val());
+
+          this.$repoSelects.each(function () {
+            var $this = $(this),
+                repoSelectVal = parseInt($this.val());
+
+            $this.find('option').show();
+
+            if (selectedVal < repoSelectVal) {
+              $this.val(selectedVal);
+            }
+
+            $this.find("option[value='" + selectedVal + "']").nextAll().hide();
+          });
+
+          if (selectedVal) {
+            this.$repoSelects.removeClass('disabled');
+
+            if (containerRegistry) {
+              containerRegistry.style.display = '';
+            }
+          } else {
+            this.$repoSelects.addClass('disabled');
+
+            if (containerRegistry) {
+              containerRegistry.style.display = 'none';
+              containerRegistryCheckbox.checked = false;
+            }
+          }
+        }.bind(this));
+    };
+
     return ProjectNew;
 
   })();
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 20b147500cf43d284ee80cd7022762f537afffa4..e1acf3c823256347c81b05a2d5d33f13de57318b 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectSelect = (function() {
     function ProjectSelect() {
@@ -23,7 +24,7 @@
                   data = groups.concat(projects);
                   return finalCallback(data);
                 };
-                return Api.groups(term, false, groupsCallback);
+                return Api.groups(term, {}, groupsCallback);
               };
             } else {
               projectsCallback = finalCallback;
@@ -72,7 +73,7 @@
                     data = groups.concat(projects);
                     return finalCallback(data);
                   };
-                  return Api.groups(query.term, false, groupsCallback);
+                  return Api.groups(query.term, {}, groupsCallback);
                 };
               } else {
                 projectsCallback = finalCallback;
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 8ca4c4279120525b19e0744df248551bee67629f..21650f5f67af46eb2d7233a2c5c1665d2cfd4b01 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectShow = (function() {
     function ProjectShow() {}
@@ -7,3 +8,5 @@
   })();
 
 }).call(this);
+
+// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 4f415b05dbc4793f29410c8b53a627c43a9cd122..3458cd89ae20c91e896dda152d95b53d9309100c 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.ProjectsList = {
     init: function() {
@@ -33,6 +34,7 @@
           $('.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"
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
similarity index 72%
rename from app/assets/javascripts/protected_branch_access_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 2fbb088fa04e2d945d85db55186b7d24102d8c11..2d60947a666e29dd114a839ce85a99906dfd0b4f 100644
--- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (global => {
   global.gl = global.gl || {};
 
@@ -10,8 +11,12 @@
         selectable: true,
         inputId: $dropdown.data('input-id'),
         fieldName: $dropdown.data('field-name'),
-        toggleLabel(item) {
-          return item.text;
+        toggleLabel(item, el) {
+          if (el.is('.is-active')) {
+            return item.text;
+          } else {
+            return 'Select';
+          }
         },
         clicked(item, $el, e) {
           e.preventDefault();
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
similarity index 90%
rename from app/assets/javascripts/protected_branch_create.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index 2efca2414dcc1400f5778a0349407ebd6b4d33bd..c45c9d8ff227a50b59545e6670b1e4e9390c6c86 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (global => {
   global.gl = global.gl || {};
 
@@ -47,9 +48,7 @@
       const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
       const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
 
-      if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
-        this.$form.find('input[type="submit"]').removeAttr('disabled');
-      }
+      this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
     }
   }
 
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
similarity index 95%
rename from app/assets/javascripts/protected_branch_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 6738dc8862dff3e10683a7ea926337d16a088524..e3f226e9a2a3b537e4d6ac16f8ee06b8db40aef7 100644
--- a/app/assets/javascripts/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 class ProtectedBranchDropdown {
   constructor(options) {
     this.onSelect = options.onSelect;
@@ -45,6 +46,7 @@ class ProtectedBranchDropdown {
   }
 
   onClickCreateWildcard() {
+    // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
     this.$dropdown.data('glDropdown').remote.execute();
     this.$dropdown.data('glDropdown').selectRowAtIndex(0);
   }
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
similarity index 91%
rename from app/assets/javascripts/protected_branch_edit.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index a59fcbfa082e2e6f337d940cc40172305f5f3090..ac3142ffb07a97f82c4eccfcdd0b96350e639dd2 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (global => {
   global.gl = global.gl || {};
 
@@ -31,13 +32,15 @@
       const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
       const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
 
+      // Do not update if one dropdown has not selected any option
+      if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; 
+
       $.ajax({
         type: 'POST',
         url: this.$wrap.data('url'),
         dataType: 'json',
         data: {
           _method: 'PATCH',
-          id: this.$wrap.data('banchId'),
           protected_branch: {
             merge_access_levels_attributes: [{
               id: this.$allowedToMergeDropdown.data('access-level-id'),
diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
similarity index 94%
rename from app/assets/javascripts/protected_branch_edit_list.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 9ff0fd12c76202031395f05ecd05f1ce55fa6c72..705378a364d0fddd27afd38b94ae19d6d0b9d803 100644
--- a/app/assets/javascripts/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (global => {
   global.gl = global.gl || {};
 
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..17e3416383162ad82c2acff987004c6e185fda5b
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+/*= require_tree . */
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index dc4d51138261f1f71a90684367ac5b289a97c5e1..df38937858f7c9adb8e052a599ca82fd62702025 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -5,15 +6,24 @@
     function Sidebar(currentUser) {
       this.toggleTodo = bind(this.toggleTodo, this);
       this.sidebar = $('aside');
+      this.removeListeners();
       this.addEventListeners();
     }
 
+    Sidebar.prototype.removeListeners = function () {
+      this.sidebar.off('click', '.sidebar-collapsed-icon');
+      $('.dropdown').off('hidden.gl.dropdown');
+      $('.dropdown').off('loading.gl.dropdown');
+      $('.dropdown').off('loaded.gl.dropdown');
+      $(document).off('click', '.js-sidebar-toggle');
+    }
+
     Sidebar.prototype.addEventListeners = function() {
       this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
       $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
       $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
       $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
-      $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
+      $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
         var $allGutterToggleIcons, $this, $thisIcon;
         e.preventDefault();
         $this = $(this);
@@ -29,9 +39,7 @@
           $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
         }
         if (!triggered) {
-          return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
-            path: '/'
-          });
+          return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
         }
       });
       return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
@@ -74,16 +82,11 @@
     };
 
     Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
-      var $todoPendingCount;
-      $todoPendingCount = $('.todos-pending-count');
-      $todoPendingCount.text(data.count);
+      $(document).trigger('todo:toggle', data.count);
+
       $btn.enable();
       $todoLoading.addClass('hidden');
-      if (data.count === 0) {
-        $todoPendingCount.addClass('hidden');
-      } else {
-        $todoPendingCount.removeClass('hidden');
-      }
+
       if (data.delete_path != null) {
         $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
         return $btnText.text($btn.data('mark-text'));
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d34346f862bcddba426526426a88968eebee9a6e..d79e6f014f6b9acc1f18cad78780b56897af3d7b 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Search = (function() {
     function Search() {
@@ -10,7 +11,7 @@
         filterable: true,
         fieldName: 'group_id',
         data: function(term, callback) {
-          return Api.groups(term, null, function(data) {
+          return Api.groups(term, {}, function(data) {
             data.unshift({
               name: 'Any'
             });
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js.es6
similarity index 67%
rename from app/assets/javascripts/search_autocomplete.js
rename to app/assets/javascripts/search_autocomplete.js.es6
index 990f6536eb2a69a4ca8b2d9425e4a8cb2a0d0066..5fa94556501560a74c0fa081a2bee0f5a8654cea 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,27 +1,22 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+/* eslint-disable */
+((global) => {
 
-  this.SearchAutocomplete = (function() {
-    var KEYCODE;
+  const KEYCODE = {
+    ESCAPE: 27,
+    BACKSPACE: 8,
+    ENTER: 13,
+    UP: 38,
+    DOWN: 40
+  };
 
-    KEYCODE = {
-      ESCAPE: 27,
-      BACKSPACE: 8,
-      ENTER: 13
-    };
-
-    function SearchAutocomplete(opts) {
-      var ref, ref1, ref2, ref3, ref4;
-      if (opts == null) {
-        opts = {};
-      }
-      this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
-      this.onClearInputClick = bind(this.onClearInputClick, this);
-      this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
-      this.onSearchInputClick = bind(this.onSearchInputClick, this);
-      this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
-      this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
-      this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
+  class SearchAutocomplete {
+    constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+      this.bindEventContext();
+      this.wrap = wrap || $('.search');
+      this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+      this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
+      this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
+      this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
       this.dropdown = this.wrap.find('.dropdown');
       this.dropdownContent = this.dropdown.find('.dropdown-content');
       this.locationBadgeEl = this.getElement('.location-badge');
@@ -33,6 +28,7 @@
       this.repositoryInputEl = this.getElement('#repository_ref');
       this.clearInput = this.getElement('.js-clear-input');
       this.saveOriginalState();
+      // Only when user is logged in
       if (gon.current_user_id) {
         this.createAutocomplete();
       }
@@ -41,19 +37,28 @@
       this.bindEvents();
     }
 
-    SearchAutocomplete.prototype.getElement = function(selector) {
+    // Finds an element inside wrapper element
+    bindEventContext() {
+      this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
+      this.onClearInputClick = this.onClearInputClick.bind(this);
+      this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
+      this.onSearchInputClick = this.onSearchInputClick.bind(this);
+      this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
+      this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+    }
+    getElement(selector) {
       return this.wrap.find(selector);
-    };
+    }
 
-    SearchAutocomplete.prototype.saveOriginalState = function() {
+    saveOriginalState() {
       return this.originalState = this.serializeState();
-    };
+    }
 
-    SearchAutocomplete.prototype.saveTextLength = function() {
+    saveTextLength() {
       return this.lastTextLength = this.searchInput.val().length;
-    };
+    }
 
-    SearchAutocomplete.prototype.createAutocomplete = function() {
+    createAutocomplete() {
       return this.searchInput.glDropdown({
         filterInputBlur: false,
         filterable: true,
@@ -68,9 +73,9 @@
         selectable: true,
         clicked: this.onClick.bind(this)
       });
-    };
+    }
 
-    SearchAutocomplete.prototype.getData = function(term, callback) {
+    getData(term, callback) {
       var _this, contents, jqXHR;
       _this = this;
       if (!term) {
@@ -80,6 +85,7 @@
         }
         return;
       }
+      // Prevent multiple ajax calls
       if (this.loadingSuggestions) {
         return;
       }
@@ -90,14 +96,17 @@
         term: term
       }, function(response) {
         var data, firstCategory, i, lastCategory, len, suggestion;
+        // Hide dropdown menu if no suggestions returns
         if (!response.length) {
           _this.disableAutocomplete();
           return;
         }
         data = [];
+        // List results
         firstCategory = true;
         for (i = 0, len = response.length; i < len; i++) {
           suggestion = response[i];
+          // Add group header before list each group
           if (lastCategory !== suggestion.category) {
             if (!firstCategory) {
               data.push('separator');
@@ -117,6 +126,7 @@
             url: suggestion.url
           });
         }
+        // Add option to proceed with the search
         if (data.length) {
           data.push('separator');
           data.push({
@@ -128,9 +138,9 @@
       }).always(function() {
         return _this.loadingSuggestions = false;
       });
-    };
+    }
 
-    SearchAutocomplete.prototype.getCategoryContents = function() {
+    getCategoryContents() {
       var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
       userId = gon.current_user_id;
       utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
@@ -163,20 +173,22 @@
         items.splice(0, 1);
       }
       return items;
-    };
+    }
 
-    SearchAutocomplete.prototype.serializeState = function() {
+    serializeState() {
       return {
+        // Search Criteria
         search_project_id: this.projectInputEl.val(),
         group_id: this.groupInputEl.val(),
         search_code: this.searchCodeInputEl.val(),
         repository_ref: this.repositoryInputEl.val(),
         scope: this.scopeInputEl.val(),
+        // Location badge
         _location: this.locationBadgeEl.text()
       };
-    };
+    }
 
-    SearchAutocomplete.prototype.bindEvents = function() {
+    bindEvents() {
       this.searchInput.on('keydown', this.onSearchInputKeyDown);
       this.searchInput.on('keyup', this.onSearchInputKeyUp);
       this.searchInput.on('click', this.onSearchInputClick);
@@ -188,10 +200,11 @@
           return _this.searchInput.focus();
         };
       })(this));
-    };
+    }
 
-    SearchAutocomplete.prototype.enableAutocomplete = function() {
+    enableAutocomplete() {
       var _this;
+      // No need to enable anything if user is not logged in
       if (!gon.current_user_id) {
         return;
       }
@@ -203,19 +216,23 @@
       }
     };
 
-    SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
+      // Saves last length of the entered text
+    onSearchInputKeyDown() {
       return this.saveTextLength();
-    };
+    }
 
-    SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
+    onSearchInputKeyUp(e) {
       switch (e.keyCode) {
         case KEYCODE.BACKSPACE:
+          // when trying to remove the location badge
           if (this.lastTextLength === 0 && this.badgePresent()) {
             this.removeLocationBadge();
           }
+          // When removing the last character and no badge is present
           if (this.lastTextLength === 1) {
             this.disableAutocomplete();
           }
+          // When removing any character from existin value
           if (this.lastTextLength > 1) {
             this.enableAutocomplete();
           }
@@ -223,61 +240,72 @@
         case KEYCODE.ESCAPE:
           this.restoreOriginalState();
           break;
+        case KEYCODE.ENTER:
+          this.disableAutocomplete();
+          break;
+        case KEYCODE.UP:
+        case KEYCODE.DOWN:
+          return;
         default:
+          // Handle the case when deleting the input value other than backspace
+          // e.g. Pressing ctrl + backspace or ctrl + x
           if (this.searchInput.val() === '') {
             this.disableAutocomplete();
           } else {
+            // We should display the menu only when input is not empty
             if (e.keyCode !== KEYCODE.ENTER) {
               this.enableAutocomplete();
             }
           }
       }
       this.wrap.toggleClass('has-value', !!e.target.value);
-    };
+    }
 
-    SearchAutocomplete.prototype.onSearchInputClick = function(e) {
+    // Avoid falsy value to be returned
+    onSearchInputClick(e) {
       return e.stopImmediatePropagation();
-    };
+    }
 
-    SearchAutocomplete.prototype.onSearchInputFocus = function() {
+    onSearchInputFocus() {
       this.isFocused = true;
       this.wrap.addClass('search-active');
       if (this.getValue() === '') {
         return this.getData();
       }
-    };
+    }
 
-    SearchAutocomplete.prototype.getValue = function() {
+    getValue() {
       return this.searchInput.val();
-    };
+    }
 
-    SearchAutocomplete.prototype.onClearInputClick = function(e) {
+   onClearInputClick(e) {
       e.preventDefault();
       return this.searchInput.val('').focus();
-    };
+    }
 
-    SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
+   onSearchInputBlur(e) {
       this.isFocused = false;
       this.wrap.removeClass('search-active');
+      // If input is blank then restore state
       if (this.searchInput.val() === '') {
         return this.restoreOriginalState();
       }
-    };
+    }
 
-    SearchAutocomplete.prototype.addLocationBadge = function(item) {
+    addLocationBadge(item) {
       var badgeText, category, value;
       category = item.category != null ? item.category + ": " : '';
       value = item.value != null ? item.value : '';
       badgeText = "" + category + value;
       this.locationBadgeEl.text(badgeText).show();
       return this.wrap.addClass('has-location-badge');
-    };
+    }
 
-    SearchAutocomplete.prototype.hasLocationBadge = function() {
+    hasLocationBadge() {
       return this.wrap.is('.has-location-badge');
     };
 
-    SearchAutocomplete.prototype.restoreOriginalState = function() {
+    restoreOriginalState() {
       var i, input, inputs, len;
       inputs = Object.keys(this.originalState);
       for (i = 0, len = inputs.length; i < len; i++) {
@@ -291,46 +319,49 @@
           value: this.originalState._location
         });
       }
-    };
+    }
 
-    SearchAutocomplete.prototype.badgePresent = function() {
+    badgePresent() {
       return this.locationBadgeEl.length;
-    };
+    }
 
-    SearchAutocomplete.prototype.resetSearchState = function() {
+    resetSearchState() {
       var i, input, inputs, len, results;
       inputs = Object.keys(this.originalState);
       results = [];
       for (i = 0, len = inputs.length; i < len; i++) {
         input = inputs[i];
+        // _location isnt a input
         if (input === '_location') {
           break;
         }
         results.push(this.getElement("#" + input).val(''));
       }
       return results;
-    };
+    }
 
-    SearchAutocomplete.prototype.removeLocationBadge = function() {
+    removeLocationBadge() {
       this.locationBadgeEl.hide();
       this.resetSearchState();
       this.wrap.removeClass('has-location-badge');
       return this.disableAutocomplete();
-    };
+    }
 
-    SearchAutocomplete.prototype.disableAutocomplete = function() {
-      this.searchInput.addClass('disabled');
-      this.dropdown.removeClass('open');
-      return this.restoreMenu();
-    };
+    disableAutocomplete() {
+      if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+        this.searchInput.addClass('disabled');
+        this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+        this.restoreMenu();
+      }
+    }
 
-    SearchAutocomplete.prototype.restoreMenu = function() {
+    restoreMenu() {
       var html;
       html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
       return this.dropdownContent.html(html);
     };
 
-    SearchAutocomplete.prototype.onClick = function(item, $el, e) {
+    onClick(item, $el, e) {
       if (location.pathname.indexOf(item.url) !== -1) {
         e.preventDefault();
         if (!this.badgePresent) {
@@ -353,8 +384,45 @@
       }
     };
 
-    return SearchAutocomplete;
+  }
+
+  global.SearchAutocomplete = SearchAutocomplete;
+
+  $(function() {
+    var $projectOptionsDataEl = $('.js-search-project-options');
+    var $groupOptionsDataEl = $('.js-search-group-options');
+    var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+
+    if ($projectOptionsDataEl.length) {
+      gl.projectOptions = gl.projectOptions || {};
+
+      var projectPath = $projectOptionsDataEl.data('project-path');
+
+      gl.projectOptions[projectPath] = {
+        name: $projectOptionsDataEl.data('name'),
+        issuesPath: $projectOptionsDataEl.data('issues-path'),
+        mrPath: $projectOptionsDataEl.data('mr-path')
+      };
+    }
+
+    if ($groupOptionsDataEl.length) {
+      gl.groupOptions = gl.groupOptions || {};
+
+      var groupPath = $groupOptionsDataEl.data('group-path');
+
+      gl.groupOptions[groupPath] = {
+        name: $groupOptionsDataEl.data('name'),
+        issuesPath: $groupOptionsDataEl.data('issues-path'),
+        mrPath: $groupOptionsDataEl.data('mr-path')
+      };
+    }
 
-  })();
+    if ($dashboardOptionsDataEl.length) {
+      gl.dashboardOptions = {
+        issuesPath: $dashboardOptionsDataEl.data('issues-path'),
+        mrPath: $dashboardOptionsDataEl.data('mr-path')
+      };
+    }
+  });
 
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 3b28332854a0ad0b2651e730c6bac236bab59314..8d8ab6dda5e1d236a1f0dba88b7ebc59ac52a83d 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -86,6 +87,7 @@
     var defaultStopCallback;
     defaultStopCallback = Mousetrap.stopCallback;
     return function(e, element, combo) {
+      // allowed shortcuts if textarea, input, contenteditable are focused
       if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
         return false;
       } else {
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index b931eab638f2d1dc3ea4dcae387b5652b7b1ea98..704a8bd3a57e7c3856bbf8086ae81be2e4d144ce 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts */
 
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index f7492a2aa5c8e6bd7a75e222d0c430d87d144e34..befe4eccdbac7d0843658f7bee58352f06ae2c33 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts */
 
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 6c78914d3386dd56159fafa8232cf89d10f77d0b..90ed42676619adf7854e0e52108f4e4a8ea87de5 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts_navigation */
 
@@ -14,8 +15,10 @@
       ShortcutsFindFile.__super__.constructor.call(this);
       _oldStopCallback = Mousetrap.stopCallback;
       Mousetrap.stopCallback = (function(_this) {
+        // override to fire shortcuts action when focus in textbox
         return function(event, element, combo) {
           if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
+            // when press up/down key in textbox, cusor prevent to move to home/end
             event.preventDefault();
             return false;
           }
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 3f3a8a9dfd9cbf70573499436c6aa135c5f2bc67..25ec7dbc067c4e1add6a58790732937f359323dc 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,7 +1,6 @@
+/* eslint-disable */
 
 /*= require mousetrap */
-
-
 /*= require shortcuts_navigation */
 
 (function() {
@@ -43,16 +42,20 @@
         if (selected.trim() === "") {
           return;
         }
+        // Put a '>' character before each non-empty line in the selection
         quote = _.map(selected.split("\n"), function(val) {
           if (val.trim() !== '') {
             return "> " + val + "\n";
           }
         });
+        // If replyField already has some content, add a newline before our quote
         separator = replyField.val().trim() !== "" && "\n" || '';
         replyField.val(function(_, current) {
           return current + separator + quote.join('') + "\n";
         });
+        // Trigger autosave for the added text
         replyField.trigger('input');
+        // Focus the input field
         return replyField.focus();
       }
     };
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 469e25482bbcbcd84d15eadf8d38b29216a89d7c..19c6b7d30abc0124410181a63b8120589ae0efc2 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts */
 
@@ -34,6 +35,9 @@
       Mousetrap.bind('g i', function() {
         return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
       });
+      Mousetrap.bind('g l', function() {
+        ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards');
+      });
       Mousetrap.bind('g m', function() {
         return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
       });
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index fb2b39e757e7f2445f9de5a0968c5df123787e4e..002e979a2c6c6eb0e56cb8ceaa4e333d97e6cc9a 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts_navigation */
 
diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js
deleted file mode 100644
index bd0c1194b361ea5f0e3c177d6d48ebf96ecda335..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/sidebar.js
+++ /dev/null
@@ -1,41 +0,0 @@
-(function() {
-  var collapsed, expanded, toggleSidebar;
-
-  collapsed = 'page-sidebar-collapsed';
-
-  expanded = 'page-sidebar-expanded';
-
-  toggleSidebar = function() {
-    $('.page-with-sidebar').toggleClass(collapsed + " " + expanded);
-    $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded");
-    if ($.cookie('pin_nav') === 'true') {
-      $('.navbar-fixed-top').toggleClass('header-pinned-nav');
-      $('.page-with-sidebar').toggleClass('page-sidebar-pinned');
-    }
-    return setTimeout((function() {
-      var niceScrollBars;
-      niceScrollBars = $('.nav-sidebar').niceScroll();
-      return niceScrollBars.updateScrollBar();
-    }), 300);
-  };
-
-  $(document).off('click', 'body').on('click', 'body', function(e) {
-    var $nav, $target, $toggle, pageExpanded;
-    if ($.cookie('pin_nav') !== 'true') {
-      $target = $(e.target);
-      $nav = $target.closest('.sidebar-wrapper');
-      pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded');
-      $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle');
-      if ($nav.length === 0 && pageExpanded && $toggle.length === 0) {
-        $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
-        return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded');
-      }
-    }
-  });
-
-  $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) {
-    e.preventDefault();
-    return toggleSidebar();
-  });
-
-}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..a23ca449c4b32913ed0408832622b52dd85c2ead
--- /dev/null
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -0,0 +1,96 @@
+/* eslint-disable */
+((global) => {
+  let singleton;
+
+  const pinnedStateCookie = 'pin_nav';
+  const sidebarBreakpoint = 1024;
+
+  const pageSelector = '.page-with-sidebar';
+  const navbarSelector = '.navbar-fixed-top';
+  const sidebarWrapperSelector = '.sidebar-wrapper';
+  const sidebarContentSelector = '.nav-sidebar';
+
+  const pinnedToggleSelector = '.js-nav-pin';
+  const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
+
+  const pinnedPageClass = 'page-sidebar-pinned';
+  const expandedPageClass = 'page-sidebar-expanded';
+
+  const pinnedNavbarClass = 'header-sidebar-pinned';
+  const expandedNavbarClass = 'header-sidebar-expanded';
+
+  class Sidebar {
+    constructor() {
+      if (!singleton) {
+        singleton = this;
+        singleton.init();
+      }
+      return singleton;
+    }
+
+    init() {
+      this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
+      this.isExpanded = (
+        window.innerWidth >= sidebarBreakpoint &&
+        $(pageSelector).hasClass(expandedPageClass)
+      );
+      $(document)
+        .on('click', sidebarToggleSelector, () => this.toggleSidebar())
+        .on('click', pinnedToggleSelector, () => this.togglePinnedState())
+        .on('click', 'html, body', (e) => this.handleClickEvent(e))
+        .on('page:change', () => this.renderState())
+        .on('todo:toggle', (e, count) => this.updateTodoCount(count));
+      this.renderState();
+    }
+
+    handleClickEvent(e) {
+      if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
+        const $target = $(e.target);
+        const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
+        const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
+        if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
+          this.toggleSidebar();
+        }
+      }
+    }
+
+    updateTodoCount(count) {
+      $('.js-todos-count').text(gl.text.addDelimiter(count));
+    }
+
+    toggleSidebar() {
+      this.isExpanded = !this.isExpanded;
+      this.renderState();
+    }
+
+    togglePinnedState() {
+      this.isPinned = !this.isPinned;
+      if (!this.isPinned) {
+        this.isExpanded = false;
+      }
+      Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
+      this.renderState();
+    }
+
+    renderState() {
+      $(pageSelector)
+        .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
+        .toggleClass(expandedPageClass, this.isExpanded);
+      $(navbarSelector)
+        .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
+        .toggleClass(expandedNavbarClass, this.isExpanded);
+
+      const $pinnedToggle = $(pinnedToggleSelector);
+      const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
+      const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
+      $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
+
+      if (this.isExpanded) {
+        setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+      }
+    }
+  }
+
+  global.Sidebar = Sidebar;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b9ae497b0e598e132c098dce19188d9cc4005ec0..8e54ca4f0dcffebcfaa83193c4148abec499342d 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -10,12 +11,13 @@
 
     ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
 
-    COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
+    COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
 
-    function SingleFileDiff(file) {
+    function SingleFileDiff(file, forceLoad, cb) {
       this.file = file;
       this.toggleDiff = bind(this.toggleDiff, this);
       this.content = $('.diff-content', this.file);
+      this.$toggleIcon = $('.diff-toggle-caret', this.file);
       this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
       this.isOpen = !this.diffForPath;
       if (this.diffForPath) {
@@ -23,28 +25,43 @@
         this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
         this.content = null;
         this.collapsedContent.after(this.loadingContent);
+        this.$toggleIcon.addClass('fa-caret-right');
       } else {
         this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
         this.content.after(this.collapsedContent);
+        this.$toggleIcon.addClass('fa-caret-down');
+      }
+      $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
+      if (forceLoad) {
+        this.toggleDiff(null, cb);
       }
-      this.collapsedContent.on('click', this.toggleDiff);
-      $('.file-title > a', this.file).on('click', this.toggleDiff);
     }
 
-    SingleFileDiff.prototype.toggleDiff = function(e) {
+    SingleFileDiff.prototype.toggleDiff = function(e, cb) {
+      var $target = $(e.target);
+      if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
       this.isOpen = !this.isOpen;
       if (!this.isOpen && !this.hasError) {
         this.content.hide();
-        return this.collapsedContent.show();
+        this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
+        this.collapsedContent.show();
+        if (typeof DiffNotesApp !== 'undefined') {
+          DiffNotesApp.compileComponents();
+        }
       } else if (this.content) {
         this.collapsedContent.hide();
-        return this.content.show();
+        this.content.show();
+        this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
+        if (typeof DiffNotesApp !== 'undefined') {
+          DiffNotesApp.compileComponents();
+        }
       } else {
-        return this.getContentHTML();
+        this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
+        return this.getContentHTML(cb);
       }
     };
 
-    SingleFileDiff.prototype.getContentHTML = function() {
+    SingleFileDiff.prototype.getContentHTML = function(cb) {
       this.collapsedContent.hide();
       this.loadingContent.show();
       $.get(this.diffForPath, (function(_this) {
@@ -57,7 +74,13 @@
             _this.hasError = true;
             _this.content = $(ERROR_HTML);
           }
-          return _this.collapsedContent.after(_this.content);
+          _this.collapsedContent.after(_this.content);
+
+          if (typeof DiffNotesApp !== 'undefined') {
+            DiffNotesApp.compileComponents();
+          }
+
+          if (cb) cb();
         };
       })(this));
     };
@@ -66,10 +89,10 @@
 
   })();
 
-  $.fn.singleFileDiff = function() {
+  $.fn.singleFileDiff = function(forceLoad, cb) {
     return this.each(function() {
-      if (!$.data(this, 'singleFileDiff')) {
-        return $.data(this, 'singleFileDiff', new SingleFileDiff(this));
+      if (!$.data(this, 'singleFileDiff') || forceLoad) {
+        return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
       }
     });
   };
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..083dc23c796d2422f2a7a89cef12c20cdda5d734
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,13 @@
+/* eslint-disable */
+/*= require_tree . */
+
+(function() {
+  $(function() {
+    var editor = ace.edit("editor")
+
+    $(".snippet-form-holder form").on('submit', function() {
+      $(".snippet-file-content").val(editor.getValue());
+    });
+  });
+
+}).call(this);
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..c3afc3f2246f2fa1acf7cc9740a7cf2abdbad41f
--- /dev/null
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -0,0 +1,12 @@
+/* eslint-disable */
+(global => {
+  global.gl = global.gl || {};
+
+  gl.SnippetsList = function() {
+    var $holder = $('.snippets-list-holder');
+
+    $holder.find('.pagination').on('ajax:success', (e, data) => {
+      $holder.replaceWith(data.html);
+    });
+  }
+})(window);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 10509313c12d7524640bc6700311bbcc92b8584e..cfd1e2204d512b34c084ab58e56dde8f9f940c0d 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.Star = (function() {
     function Star() {
@@ -10,11 +11,9 @@
           $this.parent().find('.star-count').text(data.star_count);
           if (isStarred) {
             $starSpan.removeClass('starred').text('Star');
-            gl.utils.updateTooltipTitle($this, 'Star project');
             $starIcon.removeClass('fa-star').addClass('fa-star-o');
           } else {
             $starSpan.addClass('starred').text('Unstar');
-            gl.utils.updateTooltipTitle($this, 'Unstar project');
             $starIcon.removeClass('fa-star-o').addClass('fa-star');
           }
         };
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 5e3c5983d754e9b16a93edcf0596914a257ec412..f99155936572a2538bb53b631f2079fb02665b21 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -5,10 +6,10 @@
     function Subscription(container) {
       this.toggleSubscription = bind(this.toggleSubscription, this);
       var $container;
-      $container = $(container);
-      this.url = $container.attr('data-url');
-      this.subscribe_button = $container.find('.js-subscribe-button');
-      this.subscription_status = $container.find('.subscription-status');
+      this.$container = $(container);
+      this.url = this.$container.attr('data-url');
+      this.subscribe_button = this.$container.find('.js-subscribe-button');
+      this.subscription_status = this.$container.find('.subscription-status');
       this.subscribe_button.unbind('click').click(this.toggleSubscription);
     }
 
@@ -18,17 +19,27 @@
       action = btn.find('span').text();
       current_status = this.subscription_status.attr('data-status');
       btn.addClass('disabled');
+
+      if ($('html').hasClass('issue-boards-page')) {
+        this.url = this.$container.attr('data-url');
+      }
+
       return $.post(this.url, (function(_this) {
         return function() {
           var status;
           btn.removeClass('disabled');
-          status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
-          _this.subscription_status.attr('data-status', status);
-          action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
-          btn.find('span').text(action);
-          _this.subscription_status.find('>div').toggleClass('hidden');
-          if (btn.attr('data-original-title')) {
-            return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+
+          if ($('html').hasClass('issue-boards-page')) {
+            Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
+          } else {
+            status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
+            _this.subscription_status.attr('data-status', status);
+            action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
+            btn.find('span').text(action);
+            _this.subscription_status.find('>div').toggleClass('hidden');
+            if (btn.attr('data-original-title')) {
+              return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+            }
           }
         };
       })(this));
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index d6c219603d103f3cfa56b9b445312f584e6e247e..2ca65cb762d788744eebc8834e30d09c2ce0163f 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.SubscriptionSelect = (function() {
     function SubscriptionSelect() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index dba62638c78143bb71f79eb7fd3a468ae4a219fb..77ad4f30b7a5b55df7ee9eb326aae0d5b433a36c 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,9 +1,21 @@
+/* eslint-disable */
+// Syntax Highlighter
+//
+// Applies a syntax highlighting color scheme CSS class to any element with the
+// `js-syntax-highlight` class
+//
+// ### Example Markup
+//
+//   <div class="js-syntax-highlight"></div>
+//
 (function() {
   $.fn.syntaxHighlight = function() {
     var $children;
     if ($(this).hasClass('js-syntax-highlight')) {
+      // Given the element itself, apply highlighting
       return $(this).addClass(gon.user_color_scheme);
     } else {
+      // Given a parent element, recurse to any of its applicable children
       $children = $(this).find('.js-syntax-highlight');
       if ($children.length) {
         return $children.syntaxHighlight();
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index c32ddf802199e1483123269cbcb72f30ed000145..93a3d67ee9fb1ed80a757403e41e1e71700f3d3c 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,7 +1,8 @@
+/* eslint-disable */
 /*= require ../blob/template_selector */
 
 ((global) => {
-  class IssuableTemplateSelector extends TemplateSelector {
+  class IssuableTemplateSelector extends gl.TemplateSelector {
     constructor(...args) {
       super(...args);
       this.projectPath = this.dropdown.data('project-path');
@@ -16,7 +17,13 @@
       if (initialQuery.name) this.requestFile(initialQuery);
 
       $('.reset-template', this.dropdown.parent()).on('click', () => {
-        if (this.currentTemplate) this.setInputValueToTemplateContent();
+        this.setInputValueToTemplateContent();
+      });
+
+      $('.no-template', this.dropdown.parent()).on('click', () => {
+        this.currentTemplate = '';
+        this.setInputValueToTemplateContent();
+        $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
       });
     }
 
@@ -36,16 +43,16 @@
       // to the content of the template selected.
       if (this.titleInput.val() === '') {
         // If the title has not yet been set, focus the title input and
-        // skip focusing the description input by setting `true` as the 2nd
-        // argument to `requestFileSuccess`.
-        this.requestFileSuccess(this.currentTemplate, true);
+        // skip focusing the description input by setting `true` as the
+        // `skipFocus` option to `requestFileSuccess`.
+        this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
         this.titleInput.focus();
       } else {
-        this.requestFileSuccess(this.currentTemplate);
+        this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
       }
       return;
     }
   }
 
   global.IssuableTemplateSelector = IssuableTemplateSelector;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index bd8cdde033ef98ac77a9d8254b565ace9ea520c7..0a3890e85feeb0fae6a3cd1a7f6bee21d7b6a552 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,12 +1,13 @@
+/* eslint-disable */
 ((global) => {
   class IssuableTemplateSelectors {
-    constructor(opts = {}) {
-      this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
-      this.editor = opts.editor || this.initEditor();
+    constructor({ $dropdowns, editor } = {}) {
+      this.$dropdowns = $dropdowns || $('.js-issuable-selector');
+      this.editor = editor || this.initEditor();
 
       this.$dropdowns.each((i, dropdown) => {
-        let $dropdown = $(dropdown);
-        new IssuableTemplateSelector({
+        const $dropdown = $(dropdown);
+        new gl.IssuableTemplateSelector({
           pattern: /(\.md)/,
           data: $dropdown.data('data'),
           wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
@@ -26,4 +27,4 @@
   }
 
   global.IssuableTemplateSelectors = IssuableTemplateSelectors;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
deleted file mode 100644
index 6e677fa8cc6907a6349d58d6571d5f777f3a13cd..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/todos.js
+++ /dev/null
@@ -1,144 +0,0 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
-  this.Todos = (function() {
-    function Todos(opts) {
-      var ref;
-      if (opts == null) {
-        opts = {};
-      }
-      this.allDoneClicked = bind(this.allDoneClicked, this);
-      this.doneClicked = bind(this.doneClicked, this);
-      this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
-      this.perPage = this.el.data('perPage');
-      this.clearListeners();
-      this.initBtnListeners();
-    }
-
-    Todos.prototype.clearListeners = function() {
-      $('.done-todo').off('click');
-      $('.js-todos-mark-all').off('click');
-      return $('.todo').off('click');
-    };
-
-    Todos.prototype.initBtnListeners = function() {
-      $('.done-todo').on('click', this.doneClicked);
-      $('.js-todos-mark-all').on('click', this.allDoneClicked);
-      return $('.todo').on('click', this.goToTodoUrl);
-    };
-
-    Todos.prototype.doneClicked = function(e) {
-      var $this;
-      e.preventDefault();
-      e.stopImmediatePropagation();
-      $this = $(e.currentTarget);
-      $this.disable();
-      return $.ajax({
-        type: 'POST',
-        url: $this.attr('href'),
-        dataType: 'json',
-        data: {
-          '_method': 'delete'
-        },
-        success: (function(_this) {
-          return function(data) {
-            _this.redirectIfNeeded(data.count);
-            _this.clearDone($this.closest('li'));
-            return _this.updateBadges(data);
-          };
-        })(this)
-      });
-    };
-
-    Todos.prototype.allDoneClicked = function(e) {
-      var $this;
-      e.preventDefault();
-      e.stopImmediatePropagation();
-      $this = $(e.currentTarget);
-      $this.disable();
-      return $.ajax({
-        type: 'POST',
-        url: $this.attr('href'),
-        dataType: 'json',
-        data: {
-          '_method': 'delete'
-        },
-        success: (function(_this) {
-          return function(data) {
-            $this.remove();
-            $('.js-todos-list').remove();
-            return _this.updateBadges(data);
-          };
-        })(this)
-      });
-    };
-
-    Todos.prototype.clearDone = function($row) {
-      var $ul;
-      $ul = $row.closest('ul');
-      $row.remove();
-      if (!$ul.find('li').length) {
-        return $ul.parents('.panel').remove();
-      }
-    };
-
-    Todos.prototype.updateBadges = function(data) {
-      $('.todos-pending .badge, .todos-pending-count').text(data.count);
-      return $('.todos-done .badge').text(data.done_count);
-    };
-
-    Todos.prototype.getTotalPages = function() {
-      return this.el.data('totalPages');
-    };
-
-    Todos.prototype.getCurrentPage = function() {
-      return this.el.data('currentPage');
-    };
-
-    Todos.prototype.getTodosPerPage = function() {
-      return this.el.data('perPage');
-    };
-
-    Todos.prototype.redirectIfNeeded = function(total) {
-      var currPage, currPages, newPages, pageParams, url;
-      currPages = this.getTotalPages();
-      currPage = this.getCurrentPage();
-      if (!total) {
-        location.reload();
-        return;
-      }
-      if (!currPages) {
-        return;
-      }
-      newPages = Math.ceil(total / this.getTodosPerPage());
-      url = location.href;
-      if (newPages !== currPages) {
-        if (currPages > 1 && currPage === currPages) {
-          pageParams = {
-            page: currPages - 1
-          };
-          url = gl.utils.mergeUrlParams(pageParams, url);
-        }
-        return Turbolinks.visit(url);
-      }
-    };
-
-    Todos.prototype.goToTodoUrl = function(e) {
-      var todoLink;
-      todoLink = $(this).data('url');
-      if (!todoLink) {
-        return;
-      }
-      if (e.metaKey || e.which === 2) {
-        e.preventDefault();
-        return window.open(todoLink, '_blank');
-      } else {
-        return Turbolinks.visit(todoLink);
-      }
-    };
-
-    return Todos;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..213e80825b726137ade2b32a563b619be6e1e09b
--- /dev/null
+++ b/app/assets/javascripts/todos.js.es6
@@ -0,0 +1,163 @@
+/* eslint-disable */
+((global) => {
+
+  class Todos {
+    constructor({ el } = {}) {
+      this.allDoneClicked = this.allDoneClicked.bind(this);
+      this.doneClicked = this.doneClicked.bind(this);
+      this.el = el || $('.js-todos-options');
+      this.perPage = this.el.data('perPage');
+      this.clearListeners();
+      this.initBtnListeners();
+      this.initFilters();
+    }
+
+    clearListeners() {
+      $('.done-todo').off('click');
+      $('.js-todos-mark-all').off('click');
+      return $('.todo').off('click');
+    }
+
+    initBtnListeners() {
+      $('.done-todo').on('click', this.doneClicked);
+      $('.js-todos-mark-all').on('click', this.allDoneClicked);
+      return $('.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();
+        Turbolinks.visit(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();
+        }
+      })
+    }
+
+    doneClicked(e) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+      const $target = $(e.currentTarget);
+      $target.disable();
+      return $.ajax({
+        type: 'POST',
+        url: $target.attr('href'),
+        dataType: 'json',
+        data: {
+          '_method': 'delete'
+        },
+        success: (data) => {
+          this.redirectIfNeeded(data.count);
+          this.clearDone($target.closest('li'));
+          return this.updateBadges(data);
+        }
+      });
+    }
+
+    allDoneClicked(e) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+      $target = $(e.currentTarget);
+      $target.disable();
+      return $.ajax({
+        type: 'POST',
+        url: $target.attr('href'),
+        dataType: 'json',
+        data: {
+          '_method': 'delete'
+        },
+        success: (data) => {
+          $target.remove();
+          $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
+          return this.updateBadges(data);
+        }
+      });
+    }
+
+    clearDone($row) {
+      const $ul = $row.closest('ul');
+      $row.remove();
+      if (!$ul.find('li').length) {
+        return $ul.parents('.panel').remove();
+      }
+    }
+
+    updateBadges(data) {
+      $(document).trigger('todo:toggle', data.count);
+      $('.todos-pending .badge').text(data.count);
+      return $('.todos-done .badge').text(data.done_count);
+    }
+
+    getTotalPages() {
+      return this.el.data('totalPages');
+    }
+
+    getCurrentPage() {
+      return this.el.data('currentPage');
+    }
+
+    getTodosPerPage() {
+      return this.el.data('perPage');
+    }
+
+    redirectIfNeeded(total) {
+      const currPages = this.getTotalPages();
+      const currPage = this.getCurrentPage();
+
+      // Refresh if no remaining Todos
+      if (!total) {
+        window.location.reload();
+        return;
+      }
+      // Do nothing if no pagination
+      if (!currPages) {
+        return;
+      }
+
+      const newPages = Math.ceil(total / this.getTodosPerPage());
+      let url = location.href;
+
+      if (newPages !== currPages) {
+        // Redirect to previous page if there's one available
+        if (currPages > 1 && currPage === currPages) {
+          const pageParams = {
+            page: currPages - 1
+          };
+          url = gl.utils.mergeUrlParams(pageParams, url);
+        }
+        return Turbolinks.visit(url);
+      }
+    }
+
+    goToTodoUrl(e) {
+      const todoLink = $(this).data('url');
+      if (!todoLink) {
+        return;
+      }
+      // Allow Meta-Click or Mouse3-click to open in a new tab
+      if (e.metaKey || e.which === 2) {
+        e.preventDefault();
+        return window.open(todoLink, '_blank');
+      } else {
+        return Turbolinks.visit(todoLink);
+      }
+    }
+  }
+
+  global.Todos = Todos;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 78e159a7ed97c3c397a31a224e23f4da57ccd975..70aff4b9a2f308e8b538a84e488255201d7441c6 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,7 +1,10 @@
+/* eslint-disable */
 (function() {
   this.TreeView = (function() {
     function TreeView() {
       this.initKeyNav();
+      // Code browser tree slider
+      // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
       $(".tree-content-holder .tree-item").on('click', function(e) {
         var $clickedEl, path;
         $clickedEl = $(e.target);
@@ -15,6 +18,7 @@
           }
         }
       });
+      // Show the "Loading commit data" for only the first element
       $('span.log_loading:first').removeClass('hide');
     }
 
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 9ba847fb0c2c782199845235ed4477d88049ec85..35f2b1e2b25e5426da2c086e6923bd013cd0e096 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,3 +1,8 @@
+/* eslint-disable */
+// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -15,6 +20,17 @@
       this.appId = u2fParams.app_id;
       this.challenge = u2fParams.challenge;
       this.signRequests = u2fParams.sign_requests.map(function(request) {
+        // The U2F Javascript API v1.1 requires a single challenge, with
+        // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+        // challenge per-request, which is done by copying the single challenge
+        // into every request.
+        //
+        // In either case, we don't need the per-request challenges that the server
+        // has generated, so we can remove them.
+        //
+        // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+        // This can be removed once we upgrade.
+        // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
         return _(request).omit('challenge');
       });
     }
@@ -41,6 +57,7 @@
       })(this), 10);
     };
 
+    // Rendering #
     U2FAuthenticate.prototype.templates = {
       "notSupported": "#js-authenticate-u2f-not-supported",
       "setup": '#js-authenticate-u2f-setup',
@@ -75,6 +92,8 @@
 
     U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
       this.renderTemplate('authenticated');
+      // Prefer to do this instead of interpolating using Underscore templates
+      // because of JSON escaping issues.
       return this.container.find("#js-device-response").val(deviceResponse);
     };
 
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index bc48c67c4f27e2116aa150b22a47f53481f915a6..aff605169e409edd156eefcdd30e13ec1df3c6d6 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index c87e0840df33feaab53ddf79faf4e2f419f62d64..22fbf9f3a9137740038b2dbcda946e0b29564724 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,3 +1,8 @@
+/* eslint-disable */
+// Register U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -39,6 +44,7 @@
       })(this), 10);
     };
 
+    // Rendering #
     U2FRegister.prototype.templates = {
       "notSupported": "#js-register-u2f-not-supported",
       "setup": '#js-register-u2f-setup',
@@ -73,6 +79,8 @@
 
     U2FRegister.prototype.renderRegistered = function(deviceResponse) {
       this.renderTemplate('registered');
+      // Prefer to do this instead of interpolating using Underscore templates
+      // because of JSON escaping issues.
       return this.container.find("#js-device-response").val(deviceResponse);
     };
 
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 907e640161a41998f98fde9f76936c4973eb6f2f..2eab2d5ae23b6e3c0ded842c2dec390f3154e807 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   this.U2FUtil = (function() {
     function U2FUtil() {}
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
deleted file mode 100644
index b46390ad8f43008ab8d2be72560fa1047c299806..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/user.js
+++ /dev/null
@@ -1,31 +0,0 @@
-(function() {
-  this.User = (function() {
-    function User(opts) {
-      this.opts = opts;
-      $('.profile-groups-avatars').tooltip({
-        "placement": "top"
-      });
-      this.initTabs();
-      $('.hide-project-limit-message').on('click', function(e) {
-        var path;
-        path = '/';
-        $.cookie('hide_project_limit_message', 'false', {
-          path: path
-        });
-        $(this).parents('.project-limit-message').remove();
-        return e.preventDefault();
-      });
-    }
-
-    User.prototype.initTabs = function() {
-      return new UserTabs({
-        parentEl: '.user-profile',
-        action: this.opts.action
-      });
-    };
-
-    return User;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..5e869e99fdb1a657c26f4b3064861ce6c37a7869
--- /dev/null
+++ b/app/assets/javascripts/user.js.es6
@@ -0,0 +1,32 @@
+/* eslint-disable */
+((global) => {
+  global.User = class {
+    constructor({ action }) {
+      this.action = action;
+      this.placeProfileAvatarsToTop();
+      this.initTabs();
+      this.hideProjectLimitMessage();
+    }
+
+    placeProfileAvatarsToTop() {
+      $('.profile-groups-avatars').tooltip({
+        placement: 'top'
+      });
+    }
+
+    initTabs() {
+      return new global.UserTabs({
+        parentEl: '.user-profile',
+        action: this.action
+      });
+    }
+
+    hideProjectLimitMessage() {
+      $('.hide-project-limit-message').on('click', e => {
+        e.preventDefault();
+        Cookies.set('hide_project_limit_message', 'false');
+        $(this).parents('.project-limit-message').remove();
+      });
+    }
+  }
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
deleted file mode 100644
index e5e75701feecf9b663649dac086ae9470a64d85b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/user_tabs.js
+++ /dev/null
@@ -1,119 +0,0 @@
-(function() {
-  var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
-  this.UserTabs = (function() {
-    function UserTabs(opts) {
-      this.tabShown = bind(this.tabShown, this);
-      var i, item, len, ref, ref1, ref2, ref3;
-      this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
-      if (typeof this.parentEl === 'string') {
-        this.parentEl = $(this.parentEl);
-      }
-      this._location = location;
-      this.loaded = {};
-      ref3 = this.parentEl.find('.nav-links a');
-      for (i = 0, len = ref3.length; i < len; i++) {
-        item = ref3[i];
-        this.loaded[$(item).attr('data-action')] = false;
-      }
-      this.actions = Object.keys(this.loaded);
-      this.bindEvents();
-      if (this.action === 'show') {
-        this.action = this.defaultAction;
-      }
-      this.activateTab(this.action);
-    }
-
-    UserTabs.prototype.bindEvents = function() {
-      return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
-    };
-
-    UserTabs.prototype.tabShown = function(event) {
-      var $target, action, source;
-      $target = $(event.target);
-      action = $target.data('action');
-      source = $target.attr('href');
-      this.setTab(source, action);
-      return this.setCurrentAction(action);
-    };
-
-    UserTabs.prototype.activateTab = function(action) {
-      return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
-    };
-
-    UserTabs.prototype.setTab = function(source, action) {
-      if (this.loaded[action] === true) {
-        return;
-      }
-      if (action === 'activity') {
-        this.loadActivities(source);
-      }
-      if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
-        return this.loadTab(source, action);
-      }
-    };
-
-    UserTabs.prototype.loadTab = function(source, action) {
-      return $.ajax({
-        beforeSend: (function(_this) {
-          return function() {
-            return _this.toggleLoading(true);
-          };
-        })(this),
-        complete: (function(_this) {
-          return function() {
-            return _this.toggleLoading(false);
-          };
-        })(this),
-        dataType: 'json',
-        type: 'GET',
-        url: source + ".json",
-        success: (function(_this) {
-          return function(data) {
-            var tabSelector;
-            tabSelector = 'div#' + action;
-            _this.parentEl.find(tabSelector).html(data.html);
-            _this.loaded[action] = true;
-            return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
-          };
-        })(this)
-      });
-    };
-
-    UserTabs.prototype.loadActivities = function(source) {
-      var $calendarWrap;
-      if (this.loaded['activity'] === true) {
-        return;
-      }
-      $calendarWrap = this.parentEl.find('.user-calendar');
-      $calendarWrap.load($calendarWrap.data('href'));
-      new Activities();
-      return this.loaded['activity'] = true;
-    };
-
-    UserTabs.prototype.toggleLoading = function(status) {
-      return this.parentEl.find('.loading-status .loading').toggle(status);
-    };
-
-    UserTabs.prototype.setCurrentAction = function(action) {
-      var new_state, regExp;
-      regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
-      new_state = this._location.pathname;
-      new_state = new_state.replace(/\/+$/, "");
-      new_state = new_state.replace(regExp, '');
-      if (action !== this.defaultAction) {
-        new_state += "/" + action;
-      }
-      new_state += this._location.search + this._location.hash;
-      history.replaceState({
-        turbolinks: true,
-        url: new_state
-      }, document.title, new_state);
-      return new_state;
-    };
-
-    return UserTabs;
-
-  })();
-
-}).call(this);
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..2b310da319c6e45e6201748e2c6f806164e47269
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -0,0 +1,158 @@
+/* eslint-disable */
+/*
+UserTabs
+
+Handles persisting and restoring the current tab selection and lazily-loading
+content on the Users#show page.
+
+### Example Markup
+
+   <ul class="nav-links">
+     <li class="activity-tab active">
+       <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+         Activity
+       </a>
+     </li>
+     <li class="groups-tab">
+       <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+         Groups
+       </a>
+     </li>
+     <li class="contributed-tab">
+       <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+         Contributed projects
+       </a>
+     </li>
+     <li class="projects-tab">
+       <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+         Personal projects
+       </a>
+     </li>
+    <li class="snippets-tab">
+       <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+       </a>
+     </li>
+   </ul>
+
+   <div class="tab-content">
+     <div class="tab-pane" id="activity">
+       Activity Content
+     </div>
+     <div class="tab-pane" id="groups">
+       Groups Content
+     </div>
+     <div class="tab-pane" id="contributed">
+       Contributed projects content
+     </div>
+     <div class="tab-pane" id="projects">
+      Projects content
+     </div>
+     <div class="tab-pane" id="snippets">
+       Snippets content
+     </div>
+  </div>
+
+   <div class="loading-status">
+     <div class="loading">
+      Loading Animation
+     </div>
+   </div>
+*/
+((global) => {
+  class UserTabs {
+    constructor ({ defaultAction, action, parentEl }) {
+      this.loaded = {};
+      this.defaultAction = defaultAction || 'activity';
+      this.action = action || this.defaultAction;
+      this.$parentEl = $(parentEl) || $(document);
+      this._location = window.location;
+      this.$parentEl.find('.nav-links a')
+        .each((i, navLink) => {
+          this.loaded[$(navLink).attr('data-action')] = false;
+        });
+      this.actions = Object.keys(this.loaded);
+      this.bindEvents();
+
+      if (this.action === 'show') {
+        this.action = this.defaultAction;
+      }
+
+      this.activateTab(this.action);
+    }
+
+    bindEvents() {
+      return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+        .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+    }
+
+    tabShown(event) {
+      const $target = $(event.target);
+      const action = $target.data('action');
+      const source = $target.attr('href');
+      this.setTab(source, action);
+      return this.setCurrentAction(source, action);
+    }
+
+    activateTab(action) {
+      return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
+        .tab('show');
+    }
+
+    setTab(source, action) {
+      if (this.loaded[action]) {
+        return;
+      }
+      if (action === 'activity') {
+        this.loadActivities(source);
+      }
+
+      const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+      if (loadableActions.indexOf(action) > -1) {
+        return this.loadTab(source, action);
+      }
+    }
+
+    loadTab(source, action) {
+      return $.ajax({
+        beforeSend: () => this.toggleLoading(true),
+        complete: () => this.toggleLoading(false),
+        dataType: 'json',
+        type: 'GET',
+        url: `${source}.json`,
+        success: (data) => {
+          const tabSelector = `div#${action}`;
+          this.$parentEl.find(tabSelector).html(data.html);
+          this.loaded[action] = true;
+          return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+        }
+      });
+    }
+
+    loadActivities(source) {
+      if (this.loaded['activity']) {
+        return;
+      }
+      const $calendarWrap = this.$parentEl.find('.user-calendar');
+      $calendarWrap.load($calendarWrap.data('href'));
+      new Activities();
+      return this.loaded['activity'] = true;
+    }
+
+    toggleLoading(status) {
+      return this.$parentEl.find('.loading-status .loading')
+        .toggle(status);
+    }
+
+    setCurrentAction(source, action) {
+      let new_state = source
+      new_state = new_state.replace(/\/+$/, '');
+      new_state += this._location.search + this._location.hash;
+      history.replaceState({
+        turbolinks: true,
+        url: new_state
+      }, document.title, new_state);
+      return new_state;
+    }
+  }
+  global.UserTabs = UserTabs;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..c4dde575c6ee88ec685df0e74e98aaea028abb8c
--- /dev/null
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -0,0 +1,134 @@
+/* eslint-disable */
+((global) => {
+  const debounceTimeoutDuration = 1000;
+  const invalidInputClass = 'gl-field-error-outline';
+  const successInputClass = 'gl-field-success-outline';
+  const unavailableMessageSelector = '.username .validation-error';
+  const successMessageSelector = '.username .validation-success';
+  const pendingMessageSelector = '.username .validation-pending';
+  const invalidMessageSelector = '.username .gl-field-error';
+
+  class UsernameValidator {
+    constructor() {
+      this.inputElement = $('#new_user_username');
+      this.inputDomElement = this.inputElement.get(0);
+      this.state = {
+        available: false,
+        valid: false,
+        pending: false,
+        empty: true
+      };
+
+      const debounceTimeout = _.debounce((username) => {
+        this.validateUsername(username);
+      }, debounceTimeoutDuration);
+
+      this.inputElement.on('keyup.username_check', () => {
+        const username = this.inputElement.val();
+
+        this.state.valid = this.inputDomElement.validity.valid;
+        this.state.empty = !username.length;
+
+        if (this.state.valid) {
+          return debounceTimeout(username);
+        }
+
+        this.renderState();
+      });
+
+      // Override generic field validation
+      this.inputElement.on('invalid', this.interceptInvalid.bind(this));
+    }
+
+    renderState() {
+      // Clear all state
+      this.clearFieldValidationState();
+
+      if (this.state.valid && this.state.available) {
+        return this.setSuccessState();
+      }
+
+      if (this.state.empty) {
+        return this.clearFieldValidationState();
+      }
+
+      if (this.state.pending) {
+        return this.setPendingState();
+      }
+
+      if (!this.state.available) {
+        return this.setUnavailableState();
+      }
+
+      if (!this.state.valid) {
+        return this.setInvalidState();
+      }
+    }
+
+    interceptInvalid(event) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+
+    validateUsername(username) {
+      if (this.state.valid) {
+        this.state.pending = true;
+        this.state.available = false;
+        this.renderState();
+        return $.ajax({
+          type: 'GET',
+          url: `/users/${username}/exists`,
+          dataType: 'json',
+          success: (res) => this.setAvailabilityState(res.exists)
+        });
+      }
+    }
+
+    setAvailabilityState(usernameTaken) {
+      if (usernameTaken) {
+        this.state.valid = false;
+        this.state.available = false;
+      } else {
+        this.state.available = true;
+      }
+      this.state.pending = false;
+      this.renderState();
+    }
+
+    clearFieldValidationState() {
+      this.inputElement.siblings('p').hide();
+
+      this.inputElement.removeClass(invalidInputClass)
+        .removeClass(successInputClass);
+    }
+
+    setUnavailableState() {
+      const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector);
+      this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
+      $usernameUnavailableMessage.show();
+    }
+
+    setSuccessState() {
+      const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector);
+      this.inputElement.addClass(successInputClass).removeClass(invalidInputClass);
+      $usernameSuccessMessage.show();
+    }
+
+    setPendingState() {
+      const $usernamePendingMessage = $(pendingMessageSelector);
+      if (this.state.pending) {
+        $usernamePendingMessage.show();
+      } else {
+        $usernamePendingMessage.hide();
+      }
+    }
+
+    setInvalidState() {
+      const $inputErrorMessage = $(invalidMessageSelector);
+      this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
+      $inputErrorMessage.show();
+    }
+  }
+
+  global.UsernameValidator = UsernameValidator;
+})(window);
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 8b3dbf5f5ae1f096a9dde213887ed708a12f686d..0ec878e7e60f5048b72198ebf6f79536457e2c19 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,9 +1,9 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
   this.Calendar = (function() {
     function Calendar(timestamps, calendar_activities_path) {
-      var group, i;
       this.calendar_activities_path = calendar_activities_path;
       this.clickDay = bind(this.clickDay, this);
       this.currentSelectedDate = '';
@@ -12,29 +12,46 @@
       this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
       this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
       this.months = [];
+      // Loop through the timestamps to create a group of objects
+      // The group of objects will be grouped based on the day of the week they are
       this.timestampsTmp = [];
-      i = 0;
-      group = 0;
-      _.each(timestamps, (function(_this) {
-        return function(count, date) {
-          var day, innerArray, newDate;
-          newDate = new Date(parseInt(date) * 1000);
-          day = newDate.getDay();
-          if ((day === 0 && i !== 0) || i === 0) {
-            _this.timestampsTmp.push([]);
-            group++;
-          }
-          innerArray = _this.timestampsTmp[group - 1];
-          innerArray.push({
-            count: count,
-            date: newDate,
-            day: day
-          });
-          return i++;
-        };
-      })(this));
+      var group = 0;
+
+      var today = new Date()
+      today.setHours(0, 0, 0, 0, 0);
+
+      var oneYearAgo = new Date(today);
+      oneYearAgo.setFullYear(today.getFullYear() - 1);
+
+      var days = gl.utils.getDayDifference(oneYearAgo, today);
+
+      for(var i = 0; i <= days; i++) {
+        var date = new Date(oneYearAgo);
+        date.setDate(date.getDate() + i);
+
+        var day = date.getDay();
+        var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
+
+        // Create a new group array if this is the first day of the week
+        // or if is first object
+        if ((day === 0 && i !== 0) || i === 0) {
+          this.timestampsTmp.push([]);
+          group++;
+        }
+
+        var innerArray = this.timestampsTmp[group - 1];
+        // Push to the inner array the values that will be used to render map
+        innerArray.push({
+          count: count || 0,
+          date: date,
+          day: day
+        });
+      }
+
+      // Init color functions
       this.colorKey = this.initColorKey();
       this.color = this.initColor();
+      // Init the svg element
       this.renderSvg(group);
       this.renderDays();
       this.renderMonths();
@@ -43,8 +60,22 @@
       this.initTooltips();
     }
 
+    // Add extra padding for the last month label if it is also the last column
+    Calendar.prototype.getExtraWidthPadding = function(group) {
+      var extraWidthPadding = 0;
+      var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
+      var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
+
+      if (lastColMonth != secondLastColMonth) {
+        extraWidthPadding = 3;
+      }
+
+      return extraWidthPadding;
+    }
+
     Calendar.prototype.renderSvg = function(group) {
-      return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar');
+      var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+      return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
     };
 
     Calendar.prototype.renderDays = function() {
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index b95faadc8e72f17e7cdb90eab9203622916bee02..22bee0f61876c3380ae9fc835fc2ee1fc5cce215 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,7 +1,7 @@
+/* eslint-disable */
 
 /*= require_tree . */
 
 (function() {
 
-
 }).call(this);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 65d362e072c48527734a81d2f96e9039e1ac2a72..7a2221dbaf5f625b2465dc42af9ea016debb9140 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
     slice = [].slice;
@@ -9,16 +10,23 @@
       this.usersPath = "/autocomplete/users.json";
       this.userPath = "/autocomplete/users/:id.json";
       if (currentUser != null) {
-        this.currentUser = JSON.parse(currentUser);
+        if (typeof currentUser === 'object') {
+          this.currentUser = currentUser;
+        } else {
+          this.currentUser = JSON.parse(currentUser);
+        }
       }
       $('.js-user-search').each((function(_this) {
         return function(i, dropdown) {
           var options = {};
-          var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser;
+          var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
           $dropdown = $(dropdown);
           options.projectId = $dropdown.data('project-id');
           options.showCurrentUser = $dropdown.data('current-user');
+          options.todoFilter = $dropdown.data('todo-filter');
+          options.todoStateFilter = $dropdown.data('todo-state-filter');
           showNullUser = $dropdown.data('null-user');
+          showMenuAbove = $dropdown.data('showMenuAbove');
           showAnyUser = $dropdown.data('any-user');
           firstUser = $dropdown.data('first-user');
           options.authorId = $dropdown.data('author-id');
@@ -31,9 +39,30 @@
           $value = $block.find('.value');
           $collapsedSidebar = $block.find('.sidebar-collapsed-user');
           $loading = $block.find('.block-loading').fadeOut();
+
+          var updateIssueBoardsIssue = function () {
+            $loading.fadeIn();
+            gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+              .then(function () {
+                $loading.fadeOut();
+              });
+          };
+
           $block.on('click', '.js-assign-yourself', function(e) {
             e.preventDefault();
-            return assignTo(_this.currentUser.id);
+
+            if ($dropdown.hasClass('js-issue-board-sidebar')) {
+              Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+                id: _this.currentUser.id,
+                username: _this.currentUser.username,
+                name: _this.currentUser.name,
+                avatar_url: _this.currentUser.avatar_url
+              }));
+
+              updateIssueBoardsIssue();
+            } else {
+              return assignTo(_this.currentUser.id);
+            }
           });
           assignTo = function(selected) {
             var data;
@@ -70,9 +99,10 @@
               return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
             });
           };
-          collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
-          assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+          collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+          assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
           return $dropdown.glDropdown({
+            showMenuAbove: showMenuAbove,
             data: function(term, callback) {
               var isAuthorFilter;
               isAuthorFilter = $('.js-author-search');
@@ -81,6 +111,7 @@
                 if (term.length === 0) {
                   showDivider = 0;
                   if (firstUser) {
+                    // Move current user to the front of the list
                     for (index = j = 0, len = users.length; j < len; index = ++j) {
                       obj = users[index];
                       if (obj.username === firstUser) {
@@ -115,7 +146,11 @@
                 if (showDivider) {
                   users.splice(showDivider, 0, "divider");
                 }
-                return callback(users);
+
+                callback(users);
+                if (showMenuAbove) {
+                  $dropdown.data('glDropdown').positionMenuAbove();
+                }
               });
             },
             filterable: true,
@@ -125,8 +160,8 @@
             },
             selectable: true,
             fieldName: $dropdown.data('field-name'),
-            toggleLabel: function(selected) {
-              if (selected && 'id' in selected) {
+            toggleLabel: function(selected, el) {
+              if (selected && 'id' in selected && $(el).hasClass('is-active')) {
                 if (selected.text) {
                   return selected.text;
                 } else {
@@ -136,29 +171,55 @@
                 return defaultLabel;
               }
             },
+            defaultLabel: defaultLabel,
             inputId: 'issue_assignee_id',
             hidden: function(e) {
               $selectbox.hide();
+              // display:block overrides the hide-collapse rule
               return $value.css('display', '');
             },
-            clicked: function(user) {
+            vue: $dropdown.hasClass('js-issue-board-sidebar'),
+            clicked: function(user, $el, e) {
               var isIssueIndex, isMRIndex, page, selected;
               page = $('body').data('page');
               isIssueIndex = page === 'projects:issues:index';
               isMRIndex = (page === page && page === 'projects:merge_requests:index');
-              if ($dropdown.hasClass('js-filter-bulk-update')) {
+              if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+                e.preventDefault();
+                selectedId = user.id;
                 return;
               }
-              if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+              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'));
               } else if ($dropdown.hasClass('js-filter-submit')) {
                 return $dropdown.closest('form').submit();
+              } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+                if (user.id) {
+                  Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+                    id: user.id,
+                    username: user.username,
+                    name: user.name,
+                    avatar_url: user.avatar_url
+                  }));
+                } else {
+                  Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
+                }
+
+                updateIssueBoardsIssue();
               } else {
                 selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
                 return assignTo(selected);
               }
             },
+            id: function (user) {
+              return user.id;
+            },
             renderRow: function(user) {
               var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
               username = user.username ? "@" + user.username : "";
@@ -172,6 +233,7 @@
                   img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
                 }
               }
+              // 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>";
               listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
               listClosingTags = "</a> </li>";
@@ -210,6 +272,7 @@
                 };
                 if (query.term.length === 0) {
                   if (firstUser) {
+                    // Move current user to the front of the list
                     ref = data.results;
                     for (index = j = 0, len = ref.length; j < len; index = ++j) {
                       obj = ref[index];
@@ -240,10 +303,11 @@
                   }
                 }
                 if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+                  var trimmed = query.term.trim();
                   emailUser = {
                     name: "Invite \"" + query.term + "\"",
-                    username: query.term,
-                    id: query.term
+                    username: trimmed,
+                    id: trimmed
                   };
                   data.results.unshift(emailUser);
                 }
@@ -266,6 +330,7 @@
               return _this.formatSelection.apply(_this, args);
             },
             dropdownCssClass: "ajax-users-dropdown",
+            // we do not want to escape markup since we are displaying html in results
             escapeMarkup: function(m) {
               return m;
             }
@@ -302,6 +367,10 @@
     };
 
     UsersSelect.prototype.user = function(user_id, callback) {
+      if(!/^\d+$/.test(user_id)) {
+        return false;
+      }
+
       var url;
       url = this.buildUrl(this.userPath);
       url = url.replace(':id', user_id);
@@ -313,6 +382,8 @@
       });
     };
 
+    // Return users list. Filtered by query
+    // Only active users retrieved
     UsersSelect.prototype.users = function(query, options, callback) {
       var url;
       url = this.buildUrl(this.usersPath);
@@ -325,6 +396,8 @@
           project_id: options.projectId || null,
           group_id: options.groupId || null,
           skip_ldap: options.skipLdap || null,
+          todo_filter: options.todoFilter || null,
+          todo_state_filter: options.todoStateFilter || null,
           current_user: options.showCurrentUser || null,
           push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
           author_id: options.authorId || null,
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index 35401231fbf9c817e1d804a4a9345350edbeab4b..ad9b842db3c773e508f6dc9c097c6669581ce075 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require latinise */
 
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 71236c6a27d1c552a83d1b0f6b34d94d6f186bfd..fa124e7052db59cb623c2366a844bb9fa7763d2c 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,21 +1,35 @@
-
+/* eslint-disable */
+// Zen Mode (full screen) textarea
+//
 /*= provides zen_mode:enter */
-
-
 /*= provides zen_mode:leave */
-
-
+//
 /*= require jquery.scrollTo */
-
-
 /*= require dropzone */
-
-
 /*= require mousetrap */
-
-
 /*= require mousetrap/pause */
 
+//
+// ### Events
+//
+// `zen_mode:enter`
+//
+// Fired when the "Edit in fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-enter
+//
+// `zen_mode:leave`
+//
+// Fired when the "Leave Fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-leave
+//
 (function() {
   this.ZenMode = (function() {
     function ZenMode() {
@@ -40,6 +54,7 @@
         };
       })(this));
       $(document).on('keydown', function(e) {
+        // Esc
         if (e.keyCode === 27) {
           e.preventDefault();
           return $(document).trigger('zen_mode:leave');
@@ -52,6 +67,7 @@
       this.active_backdrop = $(backdrop);
       this.active_backdrop.addClass('fullscreen');
       this.active_textarea = this.active_backdrop.find('textarea');
+      // Prevent a user-resized textarea from persisting to fullscreen
       this.active_textarea.removeAttr('style');
       return this.active_textarea.focus();
     };
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 542a53f0377f924f06dbc104f99251f0d56797e9..e3ca7f6373a147093079223127620d8e08ee1d75 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -5,6 +5,7 @@
     display: none;
     &.hide { display: block; }
   }
+
   &.open .content {
     display: block;
     &.hide { display: none; }
@@ -20,3 +21,8 @@
     .turn-off { display: block; }
   }
 }
+
+// Hide element if Vue is still working on rendering it fully.
+[v-cloak="true"] {
+  display: none !important;
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index a306b8f3f2968fec8247ab8636f44f16f890ccc4..d5cca1b10fbf7b779b1c71ebf666e2b2f1151461 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
 @import "framework/issue_box.scss";
 @import "framework/jquery.scss";
 @import "framework/lists.scss";
+@import "framework/logo.scss";
 @import "framework/markdown_area.scss";
 @import "framework/mobile.scss";
 @import "framework/modal.scss";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 1fec61bdba1ecaef6dce3eb6090c2c6530456ac5..f1d36efb3debe7baf54fda5348b41d551293d478 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -1,72 +1,52 @@
 // This file is based off animate.css 3.5.1, available here:
 // https://github.com/daneden/animate.css/blob/3.5.1/animate.css
-// 
+//
 // animate.css - http://daneden.me/animate
 // Version - 3.5.1
 // Licensed under the MIT license - http://opensource.org/licenses/MIT
-// 
+//
 // Copyright (c) 2016 Daniel Eden
 
 .animated {
-  -webkit-animation-duration: 1s;
-  animation-duration: 1s;
-  -webkit-animation-fill-mode: both;
-  animation-fill-mode: both;
-}
-
-.animated.infinite {
-  -webkit-animation-iteration-count: infinite;
-  animation-iteration-count: infinite;
-}
+  @include webkit-prefix(animation-duration, 1s);
+  @include webkit-prefix(animation-fill-mode, both);
 
-.animated.hinge {
-  -webkit-animation-duration: 2s;
-  animation-duration: 2s;
-}
+  &.infinite {
+    @include webkit-prefix(animation-iteration-count, infinite);
+  }
 
-.animated.flipOutX,
-.animated.flipOutY,
-.animated.bounceIn,
-.animated.bounceOut {
-  -webkit-animation-duration: .75s;
-  animation-duration: .75s;
-}
+  &.once {
+    @include webkit-prefix(animation-iteration-count, 1);
+  }
 
-@-webkit-keyframes pulse {
-  from {
-    -webkit-transform: scale3d(1, 1, 1);
-    transform: scale3d(1, 1, 1);
+  &.hinge {
+    @include webkit-prefix(animation-duration, 2s);
   }
 
-  50% {
-    -webkit-transform: scale3d(1.05, 1.05, 1.05);
-    transform: scale3d(1.05, 1.05, 1.05);
+  &.flipOutX,
+  &.flipOutY,
+  &.bounceIn,
+  &.bounceOut {
+    @include webkit-prefix(animation-duration, .75s);
   }
 
-  to {
-    -webkit-transform: scale3d(1, 1, 1);
-    transform: scale3d(1, 1, 1);
+  &.short {
+    @include webkit-prefix(animation-duration, 321ms);
+    @include webkit-prefix(animation-fill-mode, none);
   }
 }
 
-@keyframes pulse {
-  from {
-    -webkit-transform: scale3d(1, 1, 1);
-    transform: scale3d(1, 1, 1);
+@include keyframes(pulse) {
+  from,
+  to {
+    @include webkit-prefix(transform, scale3d(1, 1, 1));
   }
 
   50% {
-    -webkit-transform: scale3d(1.05, 1.05, 1.05);
-    transform: scale3d(1.05, 1.05, 1.05);
-  }
-
-  to {
-    -webkit-transform: scale3d(1, 1, 1);
-    transform: scale3d(1, 1, 1);
+    @include webkit-prefix(transform, scale3d(1.05, 1.05, 1.05));
   }
 }
 
 .pulse {
-  -webkit-animation-name: pulse;
-  animation-name: pulse;
+  @include webkit-prefix(animation-name, pulse);
 }
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index c79b22d4d21036cdd690fd9a9e332a6814c299d9..202ed5ae8fea5eef1cbe589a2c7315cd35efbfec 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -1,11 +1,36 @@
-.avatar {
+@mixin avatar-size($size, $margin-right) {
+  width: $size;
+  height: $size;
+  margin-right: $margin-right;
+}
+
+.avatar-circle {
   float: left;
-  margin-right: 12px;
+  margin-right: 15px;
+  border-radius: $avatar_radius;
+  border: 1px solid rgba(0, 0, 0, .1);
+  &.s16 { @include avatar-size(16px, 6px); }
+  &.s20 { @include avatar-size(20px, 7px); }
+  &.s24 { @include avatar-size(24px, 8px); }
+  &.s26 { @include avatar-size(26px, 8px); }
+  &.s32 { @include avatar-size(32px, 10px); }
+  &.s36 { @include avatar-size(36px, 10px); }
+  &.s40 { @include avatar-size(40px, 10px); }
+  &.s46 { @include avatar-size(46px, 15px); }
+  &.s48 { @include avatar-size(48px, 10px); }
+  &.s60 { @include avatar-size(60px, 12px); }
+  &.s70 { @include avatar-size(70px, 14px); }
+  &.s90 { @include avatar-size(90px, 15px); }
+  &.s110 { @include avatar-size(110px, 15px); }
+  &.s140 { @include avatar-size(140px, 15px); }
+  &.s160 { @include avatar-size(160px, 20px); }
+}
+
+.avatar {
+  @extend .avatar-circle;
   width: 40px;
   height: 40px;
   padding: 0;
-  @include border-radius($avatar_radius);
-  border: 1px solid rgba(0, 0, 0, .1);
 
   &.avatar-inline {
     float: none;
@@ -17,25 +42,9 @@
   }
 
   &.avatar-tile {
-    @include border-radius(0);
+    border-radius: 0;
     border: none;
   }
-
-  &.s16 { width: 16px; height: 16px; margin-right: 6px; }
-  &.s20 { width: 20px; height: 20px; margin-right: 7px; }
-  &.s24 { width: 24px; height: 24px; margin-right: 8px; }
-  &.s26 { width: 26px; height: 26px; margin-right: 8px; }
-  &.s32 { width: 32px; height: 32px; margin-right: 10px; }
-  &.s36 { width: 36px; height: 36px; margin-right: 10px; }
-  &.s40 { width: 40px; height: 40px; margin-right: 10px; }
-  &.s46 { width: 46px; height: 46px; margin-right: 15px; }
-  &.s48 { width: 48px; height: 48px; margin-right: 10px; }
-  &.s60 { width: 60px; height: 60px; margin-right: 12px; }
-  &.s70 { width: 70px; height: 70px; margin-right: 14px; }
-  &.s90 { width: 90px; height: 90px; margin-right: 15px; }
-  &.s110 { width: 110px; height: 110px; margin-right: 15px; }
-  &.s140 { width: 140px; height: 140px; margin-right: 20px; }
-  &.s160 { width: 160px; height: 160px; margin-right: 20px; }
 }
 
 .identicon {
@@ -54,3 +63,17 @@
   &.s140 { font-size: 72px; line-height: 138px; }
   &.s160 { font-size: 96px; line-height: 158px; }
 }
+
+.avatar-container {
+  @extend .avatar-circle;
+  overflow: hidden;
+  display: flex;
+
+  .avatar {
+    border-radius: 0;
+    border: none;
+    height: auto;
+    margin: 0;
+    align-self: center;
+  }
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 7ce203d2ec7bc0e6d3ea50b2dbf7683480baa011..7e168092522905b8ac10e365fbb06f31c753ed7c 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -19,10 +19,9 @@
 
   &.diff-collapsed {
     padding: 5px;
-    cursor: pointer;
 
-    &:hover {
-      background-color: $row-hover;
+    .click-to-expand {
+      cursor: pointer;
     }
   }
 }
@@ -129,27 +128,20 @@
   position: relative;
 
   .avatar-holder {
-    margin-bottom: 16px;
-
-    .avatar, .identicon {
+    .avatar,
+    .identicon {
       margin: 0 auto;
       float: none;
     }
 
     .identicon {
-      @include border-radius(50%);
+      border-radius: 50%;
     }
   }
 
   .cover-title {
     color: $gl-header-color;
-    margin: 0;
-    font-size: 24px;
-    font-weight: normal;
-    margin-bottom: 10px;
-    color: #4c4e54;
     font-size: 23px;
-    line-height: 1.1;
 
     h1 {
       color: $gl-gray-dark;
@@ -214,6 +206,10 @@
     }
   }
 
+  &.user-cover-block {
+    padding: 24px 0 0;
+  }
+
   .group-info {
 
     h1 {
@@ -249,6 +245,10 @@
   > .controls {
     float: right;
   }
+
+  .new-branch {
+    margin-top: 3px;
+  }
 }
 
 .content-block-small {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index f1fe1697d304798c8fc5cfef347137d17e419411..e7aff2d0cecf795408e751f3a9c613cac4defaf7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,14 +1,13 @@
 @mixin btn-default {
-  @include border-radius(3px);
+  border-radius: 3px;
   font-size: $gl-font-size;
   font-weight: 500;
   padding: $gl-vert-padding $gl-btn-padding;
 
   &:focus,
   &:active {
-    outline: none;
     background-color: $btn-active-gray;
-    @include box-shadow($gl-btn-active-background);
+    box-shadow: $gl-btn-active-background;
   }
 }
 
@@ -25,7 +24,7 @@
   &:focus {
     background-color: $hover-background;
     color: $hover-text;
-    border-color: $hover-border;;
+    border-color: $hover-border;
   }
 }
 
@@ -43,7 +42,7 @@
 
   &:active,
   &.active {
-    @include box-shadow ($gl-btn-active-background);
+    box-shadow: $gl-btn-active-background;
 
     background-color: $dark;
     border-color: $border-dark;
@@ -152,7 +151,8 @@
     @include btn-blue-medium;
   }
 
-  &.btn-info {
+  &.btn-info,
+  &.btn-register {
     @include btn-blue;
   }
 
@@ -194,16 +194,30 @@
     pointer-events: none !important;
   }
 
-  .caret {
+  .fa-caret-down,
+  .fa-caret-up {
     margin-left: 5px;
   }
 
+  &.dropdown-toggle {
+    .fa-caret-down {
+      margin-left: 3px;
+    }
+  }
+
   svg {
     height: 15px;
-    width: auto;
+    width: 15px;
     position: relative;
     top: 2px;
   }
+
+  svg,
+  .fa {
+    &:not(:last-child) {
+      margin-right: 5px;
+    }
+  }
 }
 
 .btn-lg {
@@ -227,6 +241,7 @@
   width: 100%;
   margin: 0;
   margin-bottom: 15px;
+
   &.btn {
     padding: 6px 0;
   }
@@ -251,10 +266,6 @@
       outline: none;
     }
 
-    &:focus {
-      outline: none;
-    }
-
     &:active {
       outline: none;
     }
@@ -266,7 +277,7 @@
   }
 
   .active {
-    @include box-shadow($gl-btn-active-background);
+    box-shadow: $gl-btn-active-background;
 
     border: 1px solid #c6cacf !important;
     background-color: #e4e7ed !important;
@@ -308,6 +319,7 @@
 
 .btn-build {
   margin-left: 10px;
+
   i {
     color: $gl-icon-color;
   }
@@ -315,6 +327,7 @@
 
 .clone-dropdown-btn a {
   color: $dropdown-link-color;
+
   &:hover {
     text-decoration: none;
   }
@@ -324,9 +337,16 @@
   background-color: $background-color !important;
   border: 1px solid lightgrey;
   cursor: default;
+
   &:active {
     -moz-box-shadow: inset 0 0 0 white;
     -webkit-box-shadow: inset 0 0 0 white;
     box-shadow: inset 0 0 0 white;
   }
 }
+
+@media (max-width: $screen-xs-max) {
+  .btn-wide-on-xs {
+    width: 100%;
+  }
+}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index da7bab74a32dc1e55f042f8ae51bec05e08232b2..f3b6ad88ad6f7bf084fcfa04efefe10168c465a3 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -13,10 +13,12 @@
   color: $text-color;
   background: $background-color;
 }
+
 .bs-callout h4 {
   margin-top: 0;
   margin-bottom: 5px;
 }
+
 .bs-callout p:last-child {
   margin-bottom: 0;
 }
@@ -27,16 +29,19 @@
   border-color: #eed3d7;
   color: #b94a48;
 }
+
 .bs-callout-warning {
   background-color: #faf8f0;
   border-color: #faebcc;
   color: #8a6d3b;
 }
+
 .bs-callout-info {
   background-color: #f4f8fa;
   border-color: #bce8f1;
   color: #34789a;
 }
+
 .bs-callout-success {
   background-color: #dff0d8;
   border-color: #5ca64d;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index c1e5305644ba5348549593893b0ac0397fe228f9..ad5ac589d0fc1a070703a8858bd1fcefb79f82bc 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,31 +1,31 @@
 /** COLORS **/
 .cgray { color: $gl-gray; }
-.clgray { color: #bbb }
+.clgray { color: #bbb; }
 .cred { color: $gl-text-red; }
 .cgreen { color: $gl-text-green; }
-.cdark { color: #444 }
+.cdark { color: #444; }
 
 /** COMMON CLASSES **/
 .prepend-top-0 { margin-top: 0; }
 .prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px }
+.prepend-top-10 { margin-top: 10px; }
 .prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px }
-.prepend-left-5 { margin-left: 5px }
-.prepend-left-10 { margin-left: 10px }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
 .prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left: 20px }
-.append-right-5 { margin-right: 5px }
-.append-right-10 { margin-right: 10px }
+.prepend-left-20 { margin-left: 20px; }
+.append-right-5 { margin-right: 5px; }
+.append-right-10 { margin-right: 10px; }
 .append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right: 20px }
-.append-bottom-0 { margin-bottom: 0 }
-.append-bottom-10 { margin-bottom: 10px }
-.append-bottom-15 { margin-bottom: 15px }
-.append-bottom-20 { margin-bottom: 20px }
+.append-right-20 { margin-right: 20px; }
+.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-10 { margin-bottom: 10px; }
+.append-bottom-15 { margin-bottom: 15px; }
+.append-bottom-20 { margin-bottom: 20px; }
 .append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block }
-.center { text-align: center }
+.inline { display: inline-block; }
+.center { text-align: center; }
 
 .underlined-link { text-decoration: underline; }
 .hint { font-style: italic; color: #999; }
@@ -53,7 +53,7 @@ pre {
 
   &.well-pre {
     border: 1px solid #eee;
-    background: #f9f9f9;
+    background: $gray-light;
     border-radius: 0;
     color: #555;
   }
@@ -97,6 +97,7 @@ span.update-author {
   color: #999;
   font-weight: normal;
   font-style: italic;
+
   strong {
     font-weight: bold;
     font-style: normal;
@@ -128,7 +129,7 @@ p.time {
 
 // Fix issue with notes & lists creating a bunch of bottom borders.
 li.note {
-  img { max-width: 100% }
+  img { max-width: 100%; }
   .note-title {
     li {
       border-bottom: none !important;
@@ -142,7 +143,8 @@ li.note {
   }
 }
 
-.wiki_content code, .readme code {
+.wiki_content code,
+.readme code {
   background-color: inherit;
 }
 
@@ -172,6 +174,7 @@ li.note {
   @extend .col-md-6;
   text-align: left;
   margin-top: 40px;
+
   pre {
     background: white;
     border: none;
@@ -197,6 +200,7 @@ li.note {
   background: #c67;
   color: #fff;
   font-weight: bold;
+
   a {
     color: #fff;
     text-decoration: underline;
@@ -225,8 +229,9 @@ li.note {
 
 .milestone {
   &.milestone-closed {
-    background: #f9f9f9;
+    background: $gray-light;
   }
+
   .progress {
     margin-bottom: 0;
     margin-top: 4px;
@@ -248,7 +253,7 @@ li.note {
 
 img.emoji {
   height: 20px;
-  vertical-align: middle;
+  vertical-align: top;
   width: 20px;
 }
 
@@ -286,6 +291,7 @@ table {
 
 .footer-links {
   margin-bottom: 20px;
+
   a {
     margin-right: 15px;
   }
@@ -345,7 +351,8 @@ table {
   margin-right: 10px;
 }
 
-.alert, .progress {
+.alert,
+.progress {
   margin-bottom: $gl-padding;
 }
 
@@ -367,3 +374,5 @@ table {
   margin-right: -$gl-padding;
   border-top: 1px solid $border-color;
 }
+
+.hide-bottom-border { border-bottom: none !important; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index f1635a537630cb57b4a459f4749d8dbb9341c500..583c17e4a839e515a55c7fe6ebd0c8c46f240715 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1,30 +1,21 @@
-.caret {
-  display: inline-block;
-  width: 0;
-  height: 0;
-  margin-left: 2px;
-  vertical-align: middle;
-  border-top: $caret-width-base dashed;
-  border-right: $caret-width-base solid transparent;
-  border-left: $caret-width-base solid transparent;
-}
-
-.btn-group {
-  .caret {
-    margin-left: 0;
-  }
-}
-
 .dropdown {
   position: relative;
+
+  .btn-link {
+    &:hover {
+      cursor: pointer;
+    }
+  }
 }
 
 .open {
   .dropdown-menu,
   .dropdown-menu-nav {
     display: block;
+
     @media (max-width: $screen-xs-max) {
       width: 100%;
+      min-width: 240px;
     }
   }
 
@@ -45,26 +36,28 @@
   color: $dropdown-toggle-color;
   font-size: 15px;
   text-align: left;
-  border: 1px solid $dropdown-toggle-border-color;
+  border: 1px solid $border-color;
   border-radius: $border-radius-base;
-  outline: 0;
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
 
   .fa {
     position: absolute;
-    top: 50%;
-    right: 6px;
-    margin-top: -6px;
+    top: 10px;
+    right: 8px;
     color: $dropdown-toggle-icon-color;
-    font-size: 10px;
+
     &.fa-spinner {
       font-size: 16px;
       margin-top: -8px;
     }
   }
 
+  &.no-outline {
+    outline: 0;
+  }
+
   &:hover, {
     border-color: $dropdown-toggle-hover-border-color;
 
@@ -84,6 +77,15 @@
       width: 100%;
     }
   }
+
+  // Allows dynamic-width text in the dropdown toggle.
+  // Resizes to allow long text without overflowing the container.
+  &.dynamic {
+    width: auto;
+    min-width: 160px;
+    max-width: 100%;
+    padding-right: 25px;
+  }
 }
 
 .dropdown-menu,
@@ -168,6 +170,13 @@
     &.dropdown-menu-user-link {
       line-height: 16px;
     }
+
+    .icon-play {
+      fill: $table-text-gray;
+      margin-right: 6px;
+      height: 12px;
+      width: 11px;
+    }
   }
 
   .dropdown-header {
@@ -180,6 +189,12 @@
   .separator + .dropdown-header {
     padding-top: 2px;
   }
+
+  .unclickable {
+    cursor: not-allowed;
+    padding: 5px 8px;
+    color: $dropdown-header-color;
+  }
 }
 
 .dropdown-menu-large {
@@ -262,7 +277,8 @@
   a {
     padding-left: 25px;
 
-    &.is-indeterminate, &.is-active {
+    &.is-indeterminate,
+    &.is-active {
       &::before {
         position: absolute;
         left: 5px;
@@ -360,7 +376,8 @@
   }
 }
 
-.dropdown-input-field, .default-dropdown-input {
+.dropdown-input-field,
+.default-dropdown-input {
   width: 100%;
   min-height: 30px;
   padding: 0 7px;
@@ -389,7 +406,7 @@
 
 .dropdown-content {
   max-height: 215px;
-  overflow-y: scroll;
+  overflow-y: auto;
 }
 
 .dropdown-footer {
@@ -470,7 +487,7 @@
         font-size: 20px;
         text-indent: 0;
 
-        &:before {
+        &::before {
           display: block;
           position: relative;
           top: -2px;
@@ -502,7 +519,7 @@
         background-color: transparent;
         border: 0;
 
-        .ui-icon:before {
+        .ui-icon::before {
           color: $md-link-color;
         }
       }
@@ -511,7 +528,7 @@
     .ui-datepicker-prev {
       left: 0;
 
-      .ui-icon:before {
+      .ui-icon::before {
         content: '\f104';
         text-align: left;
       }
@@ -520,7 +537,7 @@
     .ui-datepicker-next {
       right: 0;
 
-      .ui-icon:before {
+      .ui-icon::before {
         content: '\f105';
         text-align: right;
       }
@@ -576,3 +593,9 @@
   display: block;
   color: $gl-placeholder-color;
 }
+
+.dropdown-toggle-text {
+  &.is-default {
+    color: $gl-placeholder-color;
+  }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 407f1873431f823ead2ab6e2fa339223106235f7..f49d7b92a00923d8f3bedf9f2ec03df6e6ff842e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -57,15 +57,17 @@
       margin-top: -3px;
     }
   }
+
   .file-content {
     background: #fff;
 
     &.image_file {
       background: #eee;
       text-align: center;
+
       img {
-        padding: 100px;
-        max-width: 50%;
+        padding: 20px;
+        max-width: 80%;
       }
     }
 
@@ -93,34 +95,29 @@
     &.blame {
       table {
         border: none;
-        box-shadow: none;
         margin: 0;
       }
+
       tr {
         border-bottom: 1px solid #eee;
       }
+
       td {
         &:first-child {
           border-left: none;
         }
+
         &:last-child {
           border-right: none;
         }
       }
-      img.avatar {
-        border: 0 none;
-        float: none;
-        margin: 0;
-        padding: 0;
-      }
-      td.blame-commit {
-        background: #f9f9f9;
-        min-width: 350px;
 
-        .commit-author-link {
-          color: #888;
-        }
+      td.blame-commit {
+        padding: 0 10px;
+        min-width: 400px;
+        background: $gray-light;
       }
+
       td.line-numbers {
         float: none;
         border-left: 1px solid #ddd;
@@ -130,14 +127,9 @@
           margin-right: 0;
         }
       }
+
       td.lines {
         padding: 0;
-        code {
-          font-family: $monospace_font;
-        }
-        pre {
-          margin: 0;
-        }
       }
     }
 
@@ -152,8 +144,10 @@
         border-left: 1px solid $border-color;
         margin-bottom: 0;
         background: white;
+
         li {
           color: #888;
+
           p {
             margin: 0;
             color: #333;
@@ -173,7 +167,6 @@
      */
     &.code {
       padding: 0;
-      -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
     }
   }
 }
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 9209347f9bc8fab57347638fc1bc136d5c477808..19827943385a10ca75db2362a9d2f14eb46cb6e2 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,6 +1,10 @@
 .filter-item {
   margin-right: 6px;
   vertical-align: top;
+
+  &.reset-filters {
+    padding: 7px;
+  }
 }
 
 @media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 0c21d0240b36d05dff2926cd9f516339a20ce0ef..a9006de6d3ee4c36a13b9b6d692120b7a5ca4a7b 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,7 +3,8 @@
   margin: 0;
   margin-bottom: $gl-padding;
   font-size: 14px;
-  z-index: 100;
+  position: relative;
+  z-index: 1;
 
   .flash-notice {
     @extend .alert;
@@ -17,10 +18,12 @@
     margin: 0;
   }
 
-  .flash-notice, .flash-alert {
+  .flash-notice,
+  .flash-alert {
     border-radius: $border-radius-default;
 
-    .container-fluid.container-limited.flash-text {
+    .container-fluid,
+    .container-fluid.container-limited {
       background: transparent;
     }
   }
@@ -28,7 +31,8 @@
   &.flash-container-page {
     margin-bottom: 0;
 
-    .flash-notice, .flash-alert {
+    .flash-notice,
+    .flash-alert {
       border-radius: 0;
     }
   }
@@ -41,4 +45,3 @@
     }
   }
 }
-
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 43d5566154147f7323dc91353c7e9c7abc8bd25f..f0727e9688ae58d315b10fa26552fbc27956c4ac 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -9,7 +9,7 @@ input {
 input[type='text'].danger {
   background: #f2dede!important;
   border-color: #d66;
-  text-shadow: 0 1px 1px #fff
+  text-shadow: 0 1px 1px #fff;
 }
 
 .datetime-controls {
@@ -19,7 +19,6 @@ input[type='text'].danger {
 }
 
 .form-actions {
-  margin: -$gl-padding;
   margin-top: 0;
   margin-bottom: -$gl-padding;
   padding: $gl-padding;
@@ -75,17 +74,17 @@ label {
 
 .form-control {
   @include box-shadow(none);
-  border-radius: 3px;
+  border-radius: 2px;
   padding: $gl-vert-padding $gl-input-padding;
 }
 
 .select-wrapper {
   position: relative;
 
-  .caret {
+  .fa-caret-down {
     position: absolute;
     right: 10px;
-    top: $gl-padding;
+    top: 10px;
     color: $gray-darkest;
     pointer-events: none;
   }
@@ -118,9 +117,11 @@ label {
     display: table-cell;
     width: 200px !important;
   }
+
   .input-group-addon {
     background-color: #f7f8fa;
   }
+
   .input-group-addon:not(:first-child):not(:last-child) {
     border-left: 0;
     border-right: 0;
@@ -130,3 +131,40 @@ label {
 .help-block {
   margin-bottom: 0;
 }
+
+.gl-field-error {
+  color: $red-normal;
+}
+
+.gl-show-field-errors {
+  .gl-field-success-outline {
+    border: 1px solid $green-normal;
+
+    &:focus {
+      box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
+      border: 0 none;
+    }
+  }
+
+  .gl-field-error-outline {
+    border: 1px solid $red-normal;
+
+    &:focus {
+      box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
+      border: 0 none;
+    }
+  }
+
+  .gl-field-success-message {
+    color: $green-normal;
+  }
+
+  .gl-field-error-message {
+    color: $red-normal;
+  }
+
+  .gl-field-hint {
+    color: $gl-text-color;
+  }
+}
+
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index f4d35c4b4b1fe26f4a816541cb98be145bdfa618..c0de09f396808f1a5b9c77a8e4fbab9de3a56622 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -2,7 +2,7 @@
  * Styles that apply to all GFM related forms.
  */
 
-.gfm-commit, .gfm-commit_range {
+.gfm-commit_range {
   font-family: $monospace_font;
   font-size: 90%;
 }
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 3673b81f183ff0e34ea288bb34eaa4319841e104..91ab15034395312c179c0f5e79bd0a1680900547 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,55 +21,66 @@
       background: $color-darker;
     }
 
-    .nav-sidebar li {
-      a {
-        color: $color-light;
-
-        &:hover, &:focus, &:active {
-          background: $color-dark;
-        }
+    .sidebar-header,
+    .sidebar-action-buttons {
+      color: $color-light;
+      background-color: lighten($color-darker, 5%);
+    }
 
-        i {
+    .nav-sidebar {
+      li {
+        a {
           color: $color-light;
-        }
-
-        path,
-        polygon {
-          fill: $color-light;
-        }
 
-        .count {
-          color: $color-light;
-          background: $color-dark;
+          &:hover,
+          &:focus,
+          &:active {
+            background: $color-dark;
+          }
+
+          i {
+            color: $color-light;
+          }
+
+          path,
+          polygon {
+            fill: $color-light;
+          }
+
+          .count {
+            color: $color-light;
+            background: $color-dark;
+          }
+
+          svg {
+            position: relative;
+            top: 3px;
+          }
         }
 
-        svg {
-          position: relative;
-          top: 3px;
+        &.separate-item {
+          border-top: 1px solid $color;
         }
-      }
 
-      &.separate-item {
-        border-top: 1px solid $color;
-      }
-
-      &.active a {
-        color: $white-light;
-        background: $color-dark;
+        &.active a {
+          color: $white-light;
+          background: $color-dark;
 
-        &.no-highlight {
-          border: none;
-        }
+          &.no-highlight {
+            border: none;
+          }
 
-        i {
-          color: $white-light
-        }
+          i {
+            color: $white-light;
+          }
 
-        path,
-        polygon {
-          fill: $white-light;
+          path,
+          polygon {
+            fill: $white-light;
+          }
         }
       }
+
     }
   }
 }
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0c607071840d21de2639bf1ad77b8573fe905de8..5a34132112a2bba0bc1d0154b555621bcd1b9e57 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,16 +2,6 @@
  *  Application Header
  *
  */
-@mixin tanuki-logo-colors($path-color) {
-  fill: $path-color;
-  transition: all 0.8s;
-
-  &:hover,
-  &.highlight {
-    fill: lighten($path-color, 25%);
-    transition: all 0.1s;
-  }
-}
 
 header {
   transition: padding $sidebar-transition-duration;
@@ -25,7 +15,8 @@ header {
       margin: 8px 0;
       text-align: center;
 
-      #tanuki-logo, img {
+      .tanuki-logo,
+      img {
         height: 36px;
       }
     }
@@ -58,15 +49,25 @@ header {
         font-size: 18px;
         padding: 0;
         margin: ($header-height - 28) / 2 0;
-        margin-left: 10px;
+        margin-left: 8px;
         height: 28px;
         min-width: 28px;
         line-height: 28px;
         text-align: center;
 
-        &:hover, &:focus, &:active {
+        &.header-user-dropdown-toggle {
+          margin-left: 14px;
+        }
+
+        &:hover,
+        &:focus,
+        &:active {
           background-color: $background-color;
         }
+
+        .fa-caret-down {
+          font-size: 15px;
+        }
       }
 
       .navbar-toggle {
@@ -87,14 +88,10 @@ header {
       }
     }
 
-    &.header-collapsed {
-      padding: 0 16px;
-    }
-
     .side-nav-toggle {
       position: absolute;
       left: -10px;
-      margin: 6px 0;
+      margin: 7px 0;
       font-size: 18px;
       padding: 6px 10px;
       border: none;
@@ -103,10 +100,6 @@ header {
       &:hover {
         background-color: $btn-gray-hover;
       }
-
-      &:focus {
-        outline: none;
-      }
     }
   }
 
@@ -126,12 +119,17 @@ header {
     .header-logo {
       position: absolute;
       left: 50%;
-      margin-left: -18px;
       top: 7px;
       transition-duration: .3s;
       z-index: 999;
 
-      svg, img {
+      #logo {
+        position: relative;
+        left: -50%;
+      }
+
+      svg,
+      img {
         height: 36px;
       }
 
@@ -140,12 +138,18 @@ header {
       }
 
       @media (max-width: $screen-xs-max) {
-        right: 25px;
+        right: 20px;
         left: auto;
+
+        #logo {
+          left: auto;
+        }
       }
     }
 
     .title {
+      position: relative;
+      padding-right: 20px;
       margin: 0;
       font-size: 19px;
       max-width: 400px;
@@ -158,23 +162,32 @@ header {
       vertical-align: top;
       white-space: nowrap;
 
-      @media (max-width: $screen-sm-max) {
+      @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+        max-width: 300px;
+      }
+
+      @media (max-width: $screen-xs-max) {
         max-width: 190px;
       }
 
       a {
         color: $gl-text-color;
+
         &:hover {
           text-decoration: underline;
         }
       }
 
       .dropdown-toggle-caret {
-        position: relative;
-        top: -2px;
+        color: $gl-text-color;
+        border: transparent;
+        background: transparent;
+        position: absolute;
+        right: 3px;
         width: 12px;
-        line-height: 12px;
-        margin-left: 5px;
+        line-height: 19px;
+        margin-top: (($header-height - 19) / 2);
+        padding: 0;
         font-size: 10px;
         text-align: center;
         cursor: pointer;
@@ -205,26 +218,6 @@ header {
   }
 }
 
-#tanuki-logo {
-
-  #tanuki-left-ear,
-  #tanuki-right-ear,
-  #tanuki-nose {
-    @include tanuki-logo-colors($tanuki-red);
-  }
-
-  #tanuki-left-eye,
-  #tanuki-right-eye {
-    @include tanuki-logo-colors($tanuki-orange);
-  }
-
-  #tanuki-left-cheek,
-  #tanuki-right-cheek {
-    @include tanuki-logo-colors($tanuki-yellow);
-  }
-
-}
-
 @media (max-width: $screen-xs-max) {
   header .container-fluid {
     font-size: 18px;
@@ -233,7 +226,8 @@ header {
       margin: 0;
       float: none !important;
 
-      .visible-xs, .visable-sm {
+      .visible-xs,
+      .visible-sm {
         display: table-cell !important;
       }
     }
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 8bfc0d583c57f901832dda50e8df72a1fa3fecd1..ba3930e03bdc0bd286ab26891c3811bebbd8b09f 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -16,7 +16,7 @@
     margin-top: 5px;
   }
 
-  @include border-radius(3px);
+  border-radius: 3px;
   display: block;
   float: left;
   margin-right: 10px;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 8bb047db2ddbc4543a90b2a99ec2385720cf458f..7baa4296abf6e29883228a3f682cbd82b7e2ddac 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -27,3 +27,15 @@ body {
 .container-limited {
   max-width: $fixed-layout-width;
 }
+
+
+/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
+which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
+effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
+of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body  */
+
+.navbar,
+.page-gutter,
+.page-with-sidebar {
+  -webkit-overflow-scrolling: auto;
+}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 965fcc06518a350e0ee4a6836911757e5ce16274..bc0610cc4174f86f6aecafbba48c162fc5fbc518 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -14,7 +14,7 @@
     border-bottom: 1px solid #eee;
     border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 
-    &:after {
+    &::after {
       content: " ";
       display: table;
       clear: both;
@@ -38,7 +38,7 @@
 
     &.smoke { background-color: $background-color; }
 
-    &:hover {
+    &:not(.ui-sort-disabled):hover {
       background: $row-hover;
     }
 
@@ -60,6 +60,7 @@
       padding-top: 1px;
       margin: 0;
       color: $gray-dark;
+
       img {
         position: relative;
         top: 3px;
@@ -75,14 +76,16 @@
 
 
 /** light list with border-bottom between li **/
-ul.bordered-list, ul.unstyled-list {
+ul.bordered-list,
+ul.unstyled-list {
   @include basic-list;
 
   &.top-list {
     li:first-child {
       padding-top: 0;
 
-      h4, h5 {
+      h4,
+      h5 {
         margin-top: 0;
       }
     }
@@ -128,6 +131,10 @@ ul.content-list {
       color: $gl-dark-link-color;
     }
 
+    .member-group-link {
+      color: $blue-normal;
+    }
+
     .description {
       p {
         @include str-truncated;
@@ -135,10 +142,6 @@ ul.content-list {
       }
     }
 
-    .avatar {
-      margin-right: 15px;
-    }
-
     .controls {
       float: right;
 
@@ -162,6 +165,18 @@ ul.content-list {
           margin-right: 0;
         }
       }
+
+      .no-comments {
+        opacity: .5;
+      }
+    }
+
+    .member-controls {
+      float: none;
+
+      @media (min-width: $screen-sm-min) {
+        float: right;
+      }
     }
 
     // When dragging a list item
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
new file mode 100644
index 0000000000000000000000000000000000000000..429cfbe72356ef55e3b804e7c1f44a466d42ddcd
--- /dev/null
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -0,0 +1,119 @@
+@mixin tanuki-logo-colors($path-color) {
+  fill: $path-color;
+  transition: all 0.8s;
+
+  &:hover {
+    fill: lighten($path-color, 25%);
+    transition: all 0.1s;
+  }
+}
+
+.tanuki-logo {
+
+  .tanuki-left-ear,
+  .tanuki-right-ear,
+  .tanuki-nose {
+    @include tanuki-logo-colors($tanuki-red);
+  }
+
+  .tanuki-left-eye,
+  .tanuki-right-eye {
+    @include tanuki-logo-colors($tanuki-orange);
+  }
+
+  .tanuki-left-cheek,
+  .tanuki-right-cheek {
+    @include tanuki-logo-colors($tanuki-yellow);
+  }
+
+  &.animate {
+    .tanuki-shape {
+      @include webkit-prefix(animation-duration, 1.5s);
+      @include webkit-prefix(animation-iteration-count, infinite);
+    }
+
+    .tanuki-left-cheek {
+      @include include-keyframes(animate-tanuki-left-cheek) {
+        0%, 10%, 100% {
+          fill: lighten($tanuki-yellow, 25%);
+        }
+
+        90% {
+          fill: $tanuki-yellow;
+        }
+      }
+    }
+
+    .tanuki-left-eye {
+      @include include-keyframes(animate-tanuki-left-eye) {
+        10%, 80% {
+          fill: $tanuki-orange;
+        }
+
+        20%, 90% {
+          fill: lighten($tanuki-orange, 25%);
+        }
+      }
+    }
+
+    .tanuki-left-ear {
+      @include include-keyframes(animate-tanuki-left-ear) {
+        10%, 80% {
+          fill: $tanuki-red;
+        }
+
+        20%, 90% {
+          fill: lighten($tanuki-red, 25%);
+        }
+      }
+    }
+
+    .tanuki-nose {
+      @include include-keyframes(animate-tanuki-nose) {
+        20%, 70% {
+          fill: $tanuki-red;
+        }
+
+        30%, 80% {
+          fill: lighten($tanuki-red, 25%);
+        }
+      }
+    }
+
+    .tanuki-right-eye {
+      @include include-keyframes(animate-tanuki-right-eye) {
+        30%, 60% {
+          fill: $tanuki-orange;
+        }
+
+        40%, 70% {
+          fill: lighten($tanuki-orange, 25%);
+        }
+      }
+    }
+
+    .tanuki-right-ear {
+      @include include-keyframes(animate-tanuki-right-ear) {
+        30%, 60% {
+          fill: $tanuki-red;
+        }
+
+        40%, 70% {
+          fill: lighten($tanuki-red, 25%);
+        }
+      }
+    }
+
+    .tanuki-right-cheek {
+      @include include-keyframes(animate-tanuki-right-cheek) {
+        40% {
+          fill: $tanuki-yellow;
+        }
+
+        60% {
+          fill: lighten($tanuki-yellow, 25%);
+        }
+      }
+    }
+  }
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 96565da1bc9ae788741cdaa94f4bf339e5cd015e..6d28d98b2835dcd363f1e5743f2a011c48a9365c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -86,7 +86,7 @@
 }
 
 .markdown-area {
-  @include border-radius(0);
+  border-radius: 0;
   background: #fff;
   border: 1px solid #ddd;
   min-height: 140px;
@@ -147,3 +147,8 @@
     color: $gl-link-color;
   }
 }
+
+.atwho-view small.description {
+  float: right;
+  padding: 3px 5px;
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 5ec5a96a597d508767fc65fcd5b36ab4dc96577e..f84ca36d10f5d797640497b1c5e2f77db071504b 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -1,51 +1,8 @@
-/**
- * Generic mixins
- */
-@mixin box-shadow($shadow) {
-  box-shadow: $shadow;
-}
-
-@mixin border-radius($radius) {
-  border-radius: $radius;
-}
-
-@mixin border-radius-left($radius) {
-  @include border-radius($radius 0 0 $radius)
-}
-
-@mixin border-radius-right($radius) {
-  @include border-radius(0 0 $radius $radius)
-}
-
-@mixin linear-gradient($from, $to) {
-  background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
-  background-image: -webkit-linear-gradient($from, $to);
-  background-image: -moz-linear-gradient($from, $to);
-  background-image: -ms-linear-gradient($from, $to);
-  background-image: -o-linear-gradient($from, $to);
-}
-
-@mixin transition($transition) {
-  -webkit-transition: $transition;
-  -moz-transition: $transition;
-  -ms-transition: $transition;
-  -o-transition: $transition;
-  transition: $transition;
-}
-
 /**
  * Prefilled mixins
  * Mixins with fixed values
  */
 
-@mixin shade {
-  @include box-shadow(0 0 3px #ddd);
-}
-
-@mixin solid-shade {
-  @include box-shadow(0 0 0 3px #f1f1f1);
-}
-
 @mixin str-truncated($max_width: 82%) {
   display: inline-block;
   overflow: hidden;
@@ -76,7 +33,8 @@
     }
 
     &.active {
-      background: #f9f9f9;
+      background: $gray-light;
+
       a {
         font-weight: 600;
       }
@@ -94,23 +52,6 @@
   }
 }
 
-@mixin input-big {
-  height: 36px;
-  padding: 5px 10px;
-  font-size: 16px;
-  line-height: 24px;
-  color: #7f8fa4;
-  background-color: #fff;
-  border-color: #e7e9ed;
-}
-
-@mixin btn-big {
-  height: 36px;
-  padding: 5px 10px;
-  font-size: 16px;
-  line-height: 24px;
-}
-
 @mixin bulleted-list {
   > ul {
     list-style-type: disc;
@@ -123,4 +64,31 @@
       }
     }
   }
-}
\ No newline at end of file
+}
+
+@mixin dark-diff-match-line {
+  color: rgba(255, 255, 255, 0.3);
+  background: rgba(255, 255, 255, 0.1);
+}
+
+@mixin webkit-prefix($property, $value) {
+  #{'-webkit-' + $property}: $value;
+  #{$property}: $value;
+}
+
+@mixin keyframes($animation-name) {
+  @-webkit-keyframes #{$animation-name} {
+    @content;
+  }
+
+  @keyframes #{$animation-name} {
+    @content;
+  }
+}
+
+@mixin include-keyframes($animation-name) {
+  @include webkit-prefix(animation-name, $animation-name);
+  @include keyframes($animation-name) {
+    @content;
+  }
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 367c7d019441b4c5500ec106914ab05c9070cb60..c1ed43bc20fd2eaf1bd27bb4f796e01537633f55 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -79,11 +79,8 @@
     padding-left: 15px !important;
   }
 
-  .issue-info, .merge-request-info {
-    display: none;
-  }
-
-  .nav-links, .nav-links {
+  .nav-links,
+  .nav-links {
     li a {
       font-size: 14px;
       padding: 19px 10px;
@@ -103,18 +100,21 @@
 
 @media (max-width: $screen-sm-max) {
   .issues-filters {
-    .milestone-filter, .labels-filter {
+    .milestone-filter,
+    .labels-filter {
       display: none;
     }
   }
 
   .page-title {
-    .note-created-ago, .new-issue-link {
+    .note-created-ago,
+    .new-issue-link {
       display: none;
     }
   }
 
-  .issue_edited_ago, .note_edited_ago {
+  .issue_edited_ago,
+  .note_edited_ago {
     display: none;
   }
 
@@ -137,5 +137,5 @@
   font-size: 20px;
   color: #777;
   z-index: 100;
-  @include box-shadow(0 1px 2px #ddd);
+  box-shadow: 0 1px 2px #ddd;
 }
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 26ad2870aa00834bf8cec81b70514f954b5cc3e6..8cd49280e1c4657701444f6af2ba4f31fcbe7a3a 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,10 +1,9 @@
 .modal-body {
   position: relative;
-  overflow-y: auto;
   padding: 15px;
 
   .form-actions {
-    margin: -$gl-padding+1;
+    margin: -$gl-padding + 1;
     margin-top: 15px;
   }
 
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7852fc9a424321c075abb55b75178bd6ddf0bf46..ce864c2de5ef784f81fd96235e7d0be72eb0cee5 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,4 +1,4 @@
-@mixin fade($gradient-direction, $rgba, $gradient-color) {
+@mixin fade($gradient-direction, $gradient-color) {
   visibility: hidden;
   opacity: 0;
   z-index: 2;
@@ -8,10 +8,7 @@
   height: 30px;
   transition-duration: .3s;
   -webkit-transform: translateZ(0);
-  background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
-  background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
-  background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
-  background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+  background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
 
   &.scrolling {
     visibility: visible;
@@ -57,9 +54,10 @@
       color: #959494;
       border-bottom: 2px solid transparent;
 
-      &:hover, &:active, &:focus {
+      &:hover,
+      &:active,
+      &:focus {
         text-decoration: none;
-        outline: none;
       }
     }
 
@@ -71,7 +69,8 @@
     .badge {
       font-weight: normal;
       background-color: #eee;
-      color: #78a;
+      color: $btn-transparent-color;
+      vertical-align: baseline;
     }
   }
 
@@ -101,8 +100,7 @@
 
 .top-area {
   @include clearfix;
-
-  border-bottom: 1px solid #eee;
+  border-bottom: 1px solid $btn-gray-hover;
 
   .nav-text {
     padding-top: 16px;
@@ -140,7 +138,7 @@
     }
 
     li a {
-      padding: 16px 10px 11px;
+      padding: 16px 15px 11px;
     }
 
     /* Small devices (phones, tablets, 768px and lower) */
@@ -160,6 +158,7 @@
     > .dropdown {
       margin-right: $gl-padding-top;
       display: inline-block;
+      vertical-align: top;
 
       &:last-child {
         margin-right: 0;
@@ -209,16 +208,15 @@
       }
     }
 
-    .project-filter-form {
-      input {
-        background-color: $background-color;
-      }
-    }
-
     @media (max-width: $screen-xs-max) {
       padding-bottom: 0;
       width: 100%;
-      .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+
+      .btn,
+      form,
+      .dropdown,
+      .dropdown-menu-toggle,
+      .form-control {
         margin: 0 0 10px;
         display: block;
         width: 100%;
@@ -252,7 +250,8 @@
   }
 
   &.adjust {
-    .nav-text, .nav-controls {
+    .nav-text,
+    .nav-controls {
       width: auto;
     }
   }
@@ -316,13 +315,15 @@
         padding-top: 10px;
       }
 
-      a, i {
+      a,
+      i {
         color: $layout-link-gray;
       }
 
       &.active {
 
-        a, i {
+        a,
+        i {
           color: $black;
         }
 
@@ -334,12 +335,9 @@
         }
       }
 
-      .badge {
-        color: $gl-icon-color;
-      }
-
       &:hover {
-        a, i {
+        a,
+        i {
           color: $black;
         }
       }
@@ -355,7 +353,7 @@
   }
 
   .fade-right {
-    @include fade(left, rgba(255, 255, 255, 0.4), $background-color);
+    @include fade(left, $background-color);
     right: -5px;
 
     .fa {
@@ -364,7 +362,7 @@
   }
 
   .fade-left {
-    @include fade(right, rgba(255, 255, 255, 0.4), $background-color);
+    @include fade(right, $background-color);
     left: -5px;
 
     .fa {
@@ -375,6 +373,7 @@
   &.sub-nav-scroll {
 
     .fade-right {
+      @include fade(left, $dark-background-color);
       right: 0;
 
       .fa {
@@ -383,6 +382,7 @@
     }
 
     .fade-left {
+      @include fade(right, $dark-background-color);
       left: 0;
 
       .fa {
@@ -399,7 +399,7 @@
     @include scrolling-links();
 
     .fade-right {
-      @include fade(left, rgba(255, 255, 255, 0.4), $white-light);
+      @include fade(left, $white-light);
       right: -5px;
 
       .fa {
@@ -408,7 +408,7 @@
     }
 
     .fade-left {
-      @include fade(right, rgba(255, 255, 255, 0.4), $white-light);
+      @include fade(right, $white-light);
       left: -5px;
 
       .fa {
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b6f21fd8c91a20904798eee5dadb77d1817c116c..cb2c351c3688506162c967e5e6d062718086ceb1 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -7,8 +7,70 @@
   .pagination {
     padding: 0;
   }
+
+  .gap,
+  .gap:hover {
+    background-color: $gray-light;
+    padding: $gl-vert-padding;
+    cursor: default;
+  }
 }
 
 .panel > .gl-pagination {
   margin: 0;
 }
+
+/**
+ * Extra-small screen pagination.
+ */
+@media (max-width: 320px) {
+  .gl-pagination {
+    .first,
+    .last {
+      display: none;
+    }
+
+    .page {
+      display: none;
+
+      &.active {
+        display: inline;
+      }
+    }
+  }
+}
+
+/**
+ * Small screen pagination
+ */
+@media (max-width: $screen-xs) {
+  .gl-pagination {
+    .pagination li a {
+      padding: 6px 10px;
+    }
+
+    .page {
+      display: none;
+
+      &.active {
+        display: inline;
+      }
+    }
+  }
+}
+
+/**
+ * Medium screen pagination
+ */
+@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+  .gl-pagination {
+    .page {
+      display: none;
+
+      &.active,
+      &.sibling {
+        display: inline;
+      }
+    }
+  }
+}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index c6f30e144fdcedfe82350df44c72cda2b6404b86..5ba0486177fa49bfbbe3053cf2e4cc748ddf692e 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -13,6 +13,11 @@
     .dropdown-menu-toggle {
       line-height: 20px;
     }
+
+    .badge {
+      margin-top: -2px;
+      margin-left: 5px;
+    }
   }
 
   .panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 21d87cc9d341d6775b00be8e3299c811b50fd728..920ce249b9a35cdb10ab86d3d392133840924e73 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -3,7 +3,8 @@
   width: 100% !important;
 }
 
-.select2-container, .select2-container.select2-drop-above {
+.select2-container,
+.select2-container.select2-drop-above {
   .select2-choice {
     background: #fff;
     border-color: $input-border;
@@ -21,7 +22,14 @@
       padding-right: 10px;
 
       b {
-        @extend .caret;
+        display: inline-block;
+        width: 0;
+        height: 0;
+        margin-left: 2px;
+        vertical-align: middle;
+        border-top: 5px dashed;
+        border-right: 5px solid transparent;
+        border-left: 5px solid transparent;
         color: $gray-darkest;
       }
     }
@@ -39,13 +47,14 @@
 }
 
 .select2-drop {
-  @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
-  @include border-radius ($border-radius-default);
+  box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
+  border-radius: $border-radius-default;
   border: none;
   min-width: 175px;
 }
 
-.select2-results .select2-result-label {
+.select2-results .select2-result-label,
+.select2-more-results {
   padding: 10px 15px;
 }
 
@@ -54,7 +63,7 @@
 }
 
 .select2-highlighted {
-  background: #3084bb !important;
+  background: $gl-link-color !important;
 }
 
 .select2-results li.select2-result-with-children > .select2-result-label {
@@ -63,8 +72,9 @@
 }
 
 .select2-container-active {
-  .select2-choice, .select2-choices {
-    @include box-shadow(none);
+  .select2-choice,
+  .select2-choices {
+    box-shadow: none;
   }
 }
 
@@ -74,18 +84,18 @@
     outline: 0;
     background-image: none;
     background-color: $white-dark;
-    @include box-shadow($gl-btn-active-gradient);
+    box-shadow: $gl-btn-active-gradient;
   }
 }
 
 .select2-container-multi {
   .select2-choices {
-    @include border-radius($border-radius-default);
+    border-radius: $border-radius-default;
     border-color: $input-border;
     background: none;
 
     .select2-search-field input {
-      padding: $gl-padding / 2;
+      padding: 5px $gl-padding / 2;
       font-size: 13px;
       height: auto;
       font-family: inherit;
@@ -93,7 +103,7 @@
     }
 
     .select2-search-choice {
-      margin: 8px 0 0 8px;
+      margin: 5px 0 0 8px;
       box-shadow: none;
       border-color: $input-border;
       color: $gl-text-color;
@@ -115,7 +125,7 @@
   &.select2-container-active .select2-choices,
   &.select2-dropdown-open .select2-choices {
     border-color: $border-white-normal;
-    @include box-shadow($gl-btn-active-gradient);
+    box-shadow: $gl-btn-active-gradient;
   }
 }
 
@@ -129,6 +139,7 @@
 
   .select2-results {
     max-height: 350px;
+
     .select2-highlighted {
       background: $gl-primary;
     }
@@ -149,8 +160,8 @@
   background-repeat: no-repeat;
   background-position: right 0 bottom 6px;
   border: 1px solid $input-border;
-  @include border-radius($border-radius-default);
-  @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+  border-radius: $border-radius-default;
+  transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
 
   &:focus {
     border-color: $input-border-focus;
@@ -204,9 +215,11 @@
   .group-image {
     float: left;
   }
+
   .group-name {
     font-weight: bold;
   }
+
   .group-path {
     color: #999;
   }
@@ -231,6 +244,7 @@
     color: #aaa;
     font-weight: normal;
   }
+
   .namespace-path {
     margin-left: 10px;
     font-weight: bolder;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3fa4a22258dfa3eb25dda78b70c46802a2cc1134..44c445c0543950e4c6c6e521c4d9913faba07011 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,11 +1,10 @@
 .page-with-sidebar {
-  padding-top: $header-height;
-  padding-bottom: 25px;
+  padding: $header-height 0 25px;
   transition: padding $sidebar-transition-duration;
 
   &.page-sidebar-pinned {
     .sidebar-wrapper {
-      @include box-shadow(none);
+      box-shadow: none;
     }
   }
 
@@ -15,9 +14,10 @@
     bottom: 0;
     left: 0;
     height: 100%;
+    width: 0;
     overflow: hidden;
     transition: width $sidebar-transition-duration;
-    @include box-shadow(2px 0 16px 0 $black-transparent);
+    box-shadow: 2px 0 16px 0 $black-transparent;
   }
 }
 
@@ -59,6 +59,11 @@
     padding: 0 !important;
   }
 
+  .sidebar-header {
+    padding: 11px 22px 12px;
+    font-size: 20px;
+  }
+
   li {
     &.separate-item {
       padding-top: 10px;
@@ -78,7 +83,6 @@
       display: block;
       text-decoration: none;
       font-weight: normal;
-      outline: none;
 
       &:hover,
       &:active,
@@ -100,7 +104,7 @@
   .count {
     float: right;
     padding: 0 8px;
-    @include border-radius(6px);
+    border-radius: 6px;
   }
 }
 
@@ -128,10 +132,8 @@
 
     .fa {
       transition: transform .15s;
-    }
 
-    &.is-active {
-      .fa {
+      .page-sidebar-pinned & {
         transform: rotate(90deg);
       }
     }
@@ -144,6 +146,7 @@
   transition-duration: .3s;
   position: absolute;
   top: 0;
+  cursor: pointer;
 
   &:hover,
   &:focus {
@@ -152,14 +155,6 @@
   }
 }
 
-.page-sidebar-collapsed {
-  padding-left: 0;
-
-  .sidebar-wrapper {
-    width: 0;
-  }
-}
-
 .page-sidebar-expanded {
   .sidebar-wrapper {
     width: $sidebar_width;
@@ -173,9 +168,21 @@
       padding-left: $sidebar_width;
     }
   }
+
+  .merge-request-tabs-holder.affix {
+    @media (min-width: $sidebar-breakpoint) {
+      left: $sidebar_width;
+    }
+  }
+
+  &.right-sidebar-expanded {
+    .line-resolve-all-container {
+      display: none;
+    }
+  }
 }
 
-header.header-pinned-nav {
+header.header-sidebar-pinned {
   @media (min-width: $sidebar-breakpoint) {
     padding-left: ($sidebar_width + $gl-padding);
 
@@ -194,6 +201,10 @@ header.header-pinned-nav {
 
   @media (min-width: $screen-sm-min) {
     padding-right: $sidebar_collapsed_width;
+
+    .merge-request-tabs-holder.affix {
+      right: $sidebar_collapsed_width;
+    }
   }
 
   .sidebar-collapsed-icon {
@@ -216,9 +227,17 @@ header.header-pinned-nav {
 
   @media (min-width: $screen-md-min) {
     padding-right: $gutter_width;
+
+    .merge-request-tabs-holder.affix {
+      right: $gutter_width;
+    }
   }
 
   &.with-overlay {
     padding-right: $sidebar_collapsed_width;
   }
 }
+
+.right-sidebar {
+  border-left: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index b42075c98d06f44ffb70d67eb94f1c51f8e7dcf1..9a90d3794fd93ccf72ad98b231fa8dfed940ab37 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -23,7 +23,8 @@ table {
     }
 
     tr {
-      td, th {
+      td,
+      th {
         padding: 10px $gl-padding;
         line-height: 20px;
         vertical-align: middle;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 0b0bd80c3269e554fd9d5f8b4bb31e6dfb4f6ce8..875cded8b4e7adb0b091b9ef6bc8228508725487 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -45,9 +45,10 @@
 
 @media (max-width: $screen-xs-max) {
   .timeline {
-    &:before {
+    &::before {
       background: none;
     }
+
     .timeline-entry .timeline-entry-inner {
       .timeline-icon {
         display: none;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index e3154657c5416a4d37cbda04017decae406bc629..59f4594bb83d0f3df43c946b9a18247dd51ace13 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -48,31 +48,40 @@
 .clearfix {
   @include clearfix();
 }
+
 .center-block {
   @include center-block();
 }
+
 .pull-right {
   float: right !important;
 }
+
 .pull-left {
   float: left !important;
 }
+
 .hide {
   display: none;
 }
+
 .show {
   display: block !important;
 }
+
 .invisible {
   visibility: hidden;
 }
+
 .text-hide {
   @include text-hide();
 }
+
 .hidden {
   display: none !important;
   visibility: hidden !important;
 }
+
 .affix {
   position: fixed;
 }
@@ -117,7 +126,8 @@
   box-shadow: none;
 
   .panel-body {
-    form, pre {
+    form,
+    pre {
       margin: 0;
     }
 
@@ -146,6 +156,7 @@
       padding: 6px 15px;
       font-size: 13px;
       font-weight: normal;
+
       a {
         color: #777;
       }
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 371c1bf17e11383c42afa8d26e15db790ff116d5..44fe37d3a4a7628ef308863e0d7e6a9df80fe1e2 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -16,21 +16,21 @@
 // $gray-light:             lighten($gray-base, 46.7%) // #777
 // $gray-lighter:           lighten($gray-base, 93.5%) // #eee
 
-$brand-primary:  $gl-primary;
-$brand-success:  $gl-success;
-$brand-info:     $gl-info;
-$brand-warning:  $gl-warning;
-$brand-danger:   $gl-danger;
+$brand-primary: $gl-primary;
+$brand-success: $gl-success;
+$brand-info: $gl-info;
+$brand-warning: $gl-warning;
+$brand-danger: $gl-danger;
 
-$border-radius-base:        3px !default;
-$border-radius-large:       3px !default;
-$border-radius-small:       3px !default;
+$border-radius-base: 3px !default;
+$border-radius-large: 3px !default;
+$border-radius-small: 3px !default;
 
 
 //== Scaffolding
 //
-$text-color:            $gl-text-color;
-$link-color:            $gl-link-color;
+$text-color: $gl-text-color;
+$link-color: $gl-link-color;
 
 
 //== Typography
@@ -38,112 +38,112 @@ $link-color:            $gl-link-color;
 //## Font, line-height, and color for body text, headings, and more.
 
 $font-family-sans-serif: $regular_font;
-$font-family-monospace:  $monospace_font;
-$font-size-base:         $gl-font-size;
+$font-family-monospace: $monospace_font;
+$font-size-base: $gl-font-size;
 
 
 //== Components
 //
 //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 
-$padding-base-vertical:     $gl-vert-padding;
-$padding-base-horizontal:   $gl-padding;
-$component-active-color:    #fff;
-$component-active-bg:       $brand-info;
+$padding-base-vertical: $gl-vert-padding;
+$padding-base-horizontal: $gl-padding;
+$component-active-color: #fff;
+$component-active-bg: $brand-info;
 
 //== Forms
 //
 //##
 
-$input-color:                    $text-color;
-$input-border:                   $border-color;
-$input-border-focus:             $focus-border-color;
-$legend-color:                   $text-color;
+$input-color: $text-color;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
+$legend-color: $text-color;
 
 
 //== Pagination
 //
 //##
 
-$pagination-color:                     $gl-gray;
-$pagination-bg:                        #fff;
-$pagination-border:                    $border-color;
+$pagination-color: $gl-gray;
+$pagination-bg: #fff;
+$pagination-border: $border-color;
 
-$pagination-hover-color:               $gl-gray;
-$pagination-hover-bg:                  $row-hover;
-$pagination-hover-border:              $border-color;
+$pagination-hover-color: $gl-gray;
+$pagination-hover-bg: $row-hover;
+$pagination-hover-border: $border-color;
 
-$pagination-active-color:              $blue-dark;
-$pagination-active-bg:                 #fff;
-$pagination-active-border:             $border-color;
+$pagination-active-color: $blue-dark;
+$pagination-active-bg: #fff;
+$pagination-active-border: $border-color;
 
-$pagination-disabled-color:            #cdcdcd;
-$pagination-disabled-bg:               $background-color;
-$pagination-disabled-border:           $border-color;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $background-color;
+$pagination-disabled-border: $border-color;
 
 
 //== Form states and alerts
 //
 //## Define colors for form feedback states and, by default, alerts.
 
-$state-success-text:             #fff;
-$state-success-bg:               $brand-success;
-$state-success-border:           $brand-success;
+$state-success-text: #fff;
+$state-success-bg: $brand-success;
+$state-success-border: $brand-success;
 
-$state-info-text:                #fff;
-$state-info-bg:                  $brand-info;
-$state-info-border:              $brand-info;
+$state-info-text: #fff;
+$state-info-bg: $brand-info;
+$state-info-border: $brand-info;
 
-$state-warning-text:             #fff;
-$state-warning-bg:               $brand-warning;
-$state-warning-border:           $brand-warning;
+$state-warning-text: #fff;
+$state-warning-bg: $brand-warning;
+$state-warning-border: $brand-warning;
 
-$state-danger-text:              #fff;
-$state-danger-bg:                $brand-danger;
-$state-danger-border:            $brand-danger;
+$state-danger-text: #fff;
+$state-danger-bg: $brand-danger;
+$state-danger-border: $brand-danger;
 
 
 //== Alerts
 //
 //## Define alert colors, border radius, and padding.
 
-$alert-border-radius:            0;
+$alert-border-radius: 0;
 
 
 //== Panels
 //
 //##
 
-$panel-border-radius:         2px;
-$panel-default-text:       $text-color;
-$panel-default-border:     $border-color;
+$panel-border-radius: 2px;
+$panel-default-text: $text-color;
+$panel-default-border: $border-color;
 $panel-default-heading-bg: $background-color;
-$panel-footer-bg:          $background-color;
-$panel-inner-border:       $border-color;
+$panel-footer-bg: $background-color;
+$panel-inner-border: $border-color;
 
 //== Wells
 //
 //##
 
-$well-bg:                     #f9f9f9;
-$well-border:                 #eee;
+$well-bg: $gray-light;
+$well-border: #eee;
 
 //== Code
 //
 //##
 
-$code-color:                  #c7254e;
-$code-bg:                     #f9f2f4;
+$code-color: #c7254e;
+$code-bg: #f9f2f4;
 
-$kbd-color:                   #fff;
-$kbd-bg:                      #333;
+$kbd-color: #fff;
+$kbd-bg: #333;
 
 //== Buttons
 //
 //##
-$btn-default-color:              $gl-text-color;
-$btn-default-bg:                 #fff;
-$btn-default-border:             #e7e9ed;
+$btn-default-color: $gl-text-color;
+$btn-default-bg: #fff;
+$btn-default-border: #e7e9ed;
 
 //== Nav
 //
@@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding;
 //== Code
 //
 //##
-$pre-bg:           $background-color !default;
-$pre-color:        $gl-gray !default;
+$pre-bg: $background-color !default;
+$pre-color: $gl-gray !default;
 $pre-border-color: $border-color;
 
 $table-bg-accent: $background-color;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8659604cb8bf11c42f35102549fdb418308ce225..070e42d63d2e7b33b47b397a2320300aa0a87ed7 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -14,12 +14,20 @@
     margin-top: 0;
   }
 
+  // Single code lines should wrap
   code {
     font-family: $monospace_font;
-    white-space: pre;
+    white-space: pre-wrap;
     word-wrap: normal;
   }
 
+  // Multi-line code blocks should scroll horizontally
+  pre {
+    code {
+      white-space: pre;
+    }
+  }
+
   kbd {
     display: inline-block;
     padding: 3px 5px;
@@ -37,40 +45,38 @@
   }
 
   h1 {
-    font-size: 2em;
+    font-size: 1.75em;
     font-weight: 600;
-    margin: 1em 0 10px;
+    margin: 16px 0 10px;
     padding: 0 0 0.3em;
-    border-bottom: 1px solid $btn-default-border;
+    border-bottom: 1px solid $white-dark;
     color: $gl-gray-dark;
   }
 
   h2 {
-    font-size: 1.6em;
+    font-size: 1.5em;
     font-weight: 600;
-    margin: 1em 0 10px;
-    padding-bottom: 0.3em;
-    border-bottom: 1px solid $btn-default-border;
+    margin: 16px 0 10px;
     color: $gl-gray-dark;
   }
 
   h3 {
-    margin: 1em 0 10px;
-    font-size: 1.4em;
+    margin: 16px 0 10px;
+    font-size: 1.3em;
   }
 
   h4 {
-    margin: 1em 0 10px;
-    font-size: 1.25em;
+    margin: 16px 0 10px;
+    font-size: 1.2em;
   }
 
   h5 {
-    margin: 1em 0 10px;
+    margin: 16px 0 10px;
     font-size: 1em;
   }
 
   h6 {
-    margin: 1em 0 10px;
+    margin: 16px 0 10px;
     font-size: 0.95em;
   }
 
@@ -79,7 +85,12 @@
     font-size: inherit;
     padding: 8px 21px;
     margin: 12px 0;
-    border-left: 3px solid #e7e9ed;
+    border-left: 3px solid $white-dark;
+  }
+
+  blockquote:dir(rtl) {
+    border-left: 0;
+    border-right: 3px solid $white-dark;
   }
 
   blockquote p {
@@ -98,34 +109,46 @@
     @extend .table-bordered;
     margin: 12px 0;
     color: #5c5d5e;
+
     th {
       background: #f8fafc;
     }
   }
 
+  table:dir(rtl) th {
+    text-align: right;
+  }
+
   pre {
     margin: 12px 0;
     font-size: 13px;
     line-height: 1.6em;
     overflow-x: auto;
-    @include border-radius(2px);
+    border-radius: 2px;
   }
 
   p > code {
     font-weight: inherit;
   }
 
-  ul, ol {
+  ul,
+  ol {
     padding: 0;
     margin: 3px 0 3px 28px !important;
   }
 
+  ul:dir(rtl),
+  ol:dir(rtl) {
+    margin: 3px 28px 3px 0 !important;
+  }
+
   li {
     line-height: 1.6em;
   }
 
-  a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
-    &:before {
+  a[href*="/uploads/"],
+  a[href*="storage.googleapis.com/google-code-attachments/"] {
+    &::before {
       margin-right: 4px;
 
       font: normal normal normal 14px/1 FontAwesome;
@@ -135,41 +158,39 @@
       content: "\f0c6";
     }
 
-    &:hover:before {
+    &:hover::before {
       text-decoration: none;
     }
   }
 
   a.no-attachment-icon {
-    &:before {
+    &::before {
       display: none;
     }
   }
 
   /* Link to current header. */
-  h1, h2, h3, h4, h5, h6 {
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
     position: relative;
 
     a.anchor {
-      // Setting `display: none` would prevent the anchor being scrolled to, so
-      // instead we set the height to 0 and it gets updated on hover.
-      height: 0;
+      left: -16px;
+      position: absolute;
+      text-decoration: none;
+
+      &::after {
+        content: image-url('icon_anchor.svg');
+        visibility: hidden;
+      }
     }
 
-    &:hover > a.anchor {
-      $size: 14px;
-      position: absolute;
-      right: 100%;
-      top: 50%;
-      margin-top: -11px;
-      margin-right: 0;
-      padding-right: 15px;
-      display: inline-block;
-      width: $size;
-      height: $size;
-      background-image: image-url("icon-link.png");
-      background-size: contain;
-      background-repeat: no-repeat;
+    &:hover > a.anchor::after {
+      visibility: visible;
     }
   }
 }
@@ -202,8 +223,13 @@ body {
   margin: 12px 7px;
 }
 
-h1, h2, h3, h4, h5, h6 {
-  color: $gl-header-color;
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  color: $gl-title-color;
   font-weight: 600;
 }
 
@@ -260,7 +286,10 @@ a > code {
   text-decoration: line-through;
 }
 
-h1, h2, h3, h4 {
+h1,
+h2,
+h3,
+h4 {
   small {
     color: $gl-gray;
   }
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ca720022539cfe006cee1645e2d75e7502278ee8..e0d00759c9cbbfc7df3acefe3ef8796f19aecb12 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -9,98 +9,19 @@ $gutter_inner_width: 258px;
 $sidebar-transition-duration: .15s;
 $sidebar-breakpoint: 1024px;
 
-/*
- * UI elements
- */
-$border-color:          #e5e5e5;
-$focus-border-color:    #3aabf0;
-$table-border-color:    #f0f0f0;
-$background-color:      #fafafa;
-$dark-background-color: #f5f5f5;
-$table-text-gray:       #8f8f8f;
-
-/*
- * Text
- */
-$gl-font-size:         15px;
-$gl-title-color:       #333;
-$gl-text-color:        #5c5c5c;
-$gl-text-green:        #4a2;
-$gl-text-red:          #d12f19;
-$gl-text-orange:       #d90;
-$gl-link-color:        #3084bb;
-$gl-dark-link-color:   #333;
-$gl-placeholder-color: #8f8f8f;
-$gl-icon-color:        $gl-placeholder-color;
-$gl-grayish-blue:      #7f8fa4;
-$gl-gray:              $gl-text-color;
-$gl-gray-dark:         #313236;
-$gl-header-color:      $gl-title-color;
-
-/*
- * Lists
- */
-$list-font-size:   $gl-font-size;
-$list-title-color: $gl-title-color;
-$list-text-color:  $gl-text-color;
-$list-text-height: 42px;
-
-/*
- * Markdown
- */
-$md-text-color: $gl-text-color;
-$md-link-color: $gl-link-color;
-
-/*
- * Code
- */
-$code_font_size: 13px;
-$code_line_height: 1.5;
-
-/*
- * Padding
- */
-$gl-padding: 16px;
-$gl-btn-padding: 10px;
-$gl-input-padding: 10px;
-$gl-vert-padding: 6px;
-$gl-padding-top: 10px;
-$gl-sidebar-padding: 22px;
-
-/*
- * Misc
- */
-$row-hover: #f7faff;
-$row-hover-border: #b2d7ff;
-$progress-color: #c0392b;
-$avatar_radius: 50%;
-$header-height: 50px;
-$fixed-layout-width: 1280px;
-$gl-avatar-size: 40px;
-$error-exclamation-point: #e62958;
-$border-radius-default: 2px;
-$btn-transparent-color: #8f8f8f;
-$settings-icon-size: 18px;
-$provider-btn-group-border: #e5e5e5;
-$provider-btn-not-active-color: #4688f1;
-$link-underline-blue: #4a8bee;
-$layout-link-gray: #7e7c7c;
-$todo-alert-blue: #428bca;
-$btn-side-margin: 10px;
-$btn-sm-side-margin: 7px;
-$btn-xs-side-margin: 5px;
-
 /*
  * Color schema
  */
-
 $white-light: #fff;
 $white-normal: #ededed;
 $white-dark: #ececec;
 
-$gray-light: #faf9f9;
+$gray-lightest: #fdfdfd;
+$gray-light: #fafafa;
+$gray-lighter: #f9f9f9;
 $gray-normal: #f5f5f5;
 $gray-dark: #ededed;
+$gray-darker: #eee;
 $gray-darkest: #c9c9c9;
 
 $green-light: #38ae67;
@@ -115,6 +36,8 @@ $blue-medium-light: #3498cb;
 $blue-medium: #2f8ebf;
 $blue-medium-dark: #2d86b4;
 
+$blue-light-transparent: rgba(44, 159, 216, 0.05);
+
 $orange-light: #fc8a51;
 $orange-normal: #e75e40;
 $orange-dark: #ce5237;
@@ -134,6 +57,7 @@ $border-gray-light: #dcdcdc;
 $border-gray-normal: #d7d7d7;
 $border-gray-dark: #c6cacf;
 
+$border-green-extra-light: #9adb84;
 $border-green-light: #2faa60;
 $border-green-normal: #2ca05b;
 $border-green-dark: #279654;
@@ -150,13 +74,98 @@ $border-red-light: #d22852;
 $border-red-normal: #ca264f;
 $border-red-dark: darken($border-red-normal, 5%);
 
-$help-well-bg: #fafafa;
+$help-well-bg: $gray-light;
 $help-well-border: #e5e5e5;
 
 $warning-message-bg: #fbf2d9;
 $warning-message-color: #9e8e60;
 $warning-message-border: #f0e2bb;
 
+/*
+ * UI elements
+ */
+$border-color: #e5e5e5;
+$focus-border-color: #3aabf0;
+$table-border-color: #f0f0f0;
+$background-color: $gray-light;
+$dark-background-color: #f5f5f5;
+$table-text-gray: #8f8f8f;
+$widget-expand-item: #e8f2f7;
+$widget-inner-border: #eef0f2;
+
+/*
+ * Text
+ */
+$gl-font-size: 15px;
+$gl-title-color: #333;
+$gl-text-color: #5c5c5c;
+$gl-text-color-light: #8c8c8c;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
+$gl-link-color: #3777b0;
+$gl-dark-link-color: #333;
+$gl-placeholder-color: #8f8f8f;
+$gl-icon-color: $gl-placeholder-color;
+$gl-grayish-blue: #7f8fa4;
+$gl-gray: $gl-text-color;
+$gl-gray-dark: #313236;
+$gl-gray-light: $gl-placeholder-color;
+$gl-header-color: #4c4e54;
+
+/*
+ * Lists
+ */
+$list-font-size: $gl-font-size;
+$list-title-color: $gl-title-color;
+$list-text-color: $gl-text-color;
+$list-text-height: 42px;
+
+/*
+ * Markdown
+ */
+$md-text-color: $gl-text-color;
+$md-link-color: $gl-link-color;
+
+/*
+ * Code
+ */
+$code_font_size: 13px;
+$code_line_height: 1.5;
+
+/*
+ * Padding
+ */
+$gl-padding: 16px;
+$gl-btn-padding: 10px;
+$gl-input-padding: 10px;
+$gl-vert-padding: 6px;
+$gl-padding-top: 10px;
+$gl-sidebar-padding: 22px;
+
+/*
+ * Misc
+ */
+$row-hover: #f7faff;
+$row-hover-border: #b2d7ff;
+$progress-color: #c0392b;
+$avatar_radius: 50%;
+$header-height: 50px;
+$fixed-layout-width: 1280px;
+$gl-avatar-size: 40px;
+$error-exclamation-point: #e62958;
+$border-radius-default: 2px;
+$btn-transparent-color: #8f8f8f;
+$settings-icon-size: 18px;
+$provider-btn-group-border: #e5e5e5;
+$provider-btn-not-active-color: #4688f1;
+$link-underline-blue: #4a8bee;
+$layout-link-gray: #7e7c7c;
+$todo-alert-blue: #428bca;
+$btn-side-margin: 10px;
+$btn-sm-side-margin: 7px;
+$btn-xs-side-margin: 5px;
+
 /* tanuki logo colors */
 $tanuki-red: #e24329;
 $tanuki-orange: #fc6d26;
@@ -186,9 +195,9 @@ $line-removed-dark: #fac5cd;
 $line-number-old: #f9d7dc;
 $line-number-new: #ddfbe6;
 $line-number-select: #fbf2da;
-$match-line: #fafafa;
+$match-line: $gray-light;
 $table-border-gray: #f0f0f0;
-$line-target-blue: #eaf3fc;
+$line-target-blue: #f6faff;
 $line-select-yellow: #fcf8e7;
 $line-select-yellow-dark: #f0e2bd;
 
@@ -267,7 +276,13 @@ $zen-control-hover-color: #111;
 $calendar-header-color: #b8b8b8;
 $calendar-hover-bg: #ecf3fe;
 $calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: #faf9f9;
+$calendar-unselectable-bg: $gray-light;
+
+/*
+ *  Cycle Analytics
+ */
+$cycle-analytics-box-padding: 30px;
+$cycle-analytics-box-text-color: #8c8c8c;
 
 /*
  * Personal Access Tokens
@@ -276,3 +291,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
 
 $ci-output-bg: #1d1f21;
 $ci-text-color: #c5c8c6;
+
+$issue-boards-font-size: 15px;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 77a73dc379b43b253781c7b2232c5c8b193c2ba0..d22d9b014958fef4cc68b9777f943537050de995 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,43 +1,53 @@
 /* https://github.com/MozMorris/tomorrow-pygments */
 .code.dark {
   // Line numbers
-  .line-numbers, .diff-line-num {
+  .line-numbers,
+  .diff-line-num {
     background-color: #1d1f21;
   }
 
-  .diff-line-num, .diff-line-num a {
+  .diff-line-num,
+  .diff-line-num a {
     color: rgba(255, 255, 255, 0.3);
   }
 
   // Code itself
-  pre.code, .diff-line-num {
+  pre.code,
+  .diff-line-num {
     border-color: #666;
   }
 
-  &, pre.code, .line_holder .line_content {
+  &,
+  pre.code,
+  .line_holder .line_content {
     background-color: #1d1f21;
     color: #c5c8c6;
   }
 
   // Diff line
   .line_holder {
+    &.match .line_content {
+      @include dark-diff-match-line;
+    }
+
     td.diff-line-num.hll:not(.empty-cell),
     td.line_content.hll:not(.empty-cell) {
       background-color: #557;
       border-color: darken(#557, 15%);
     }
 
-    .diff-line-num.new, .line_content.new {
+    .diff-line-num.new,
+    .line_content.new {
       @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
     }
 
-    .diff-line-num.old, .line_content.old {
+    .diff-line-num.old,
+    .line_content.old {
       @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
@@ -52,68 +62,68 @@
     color: #000 !important;
   }
 
-  .hll { background-color: #373b41 }
-  .c { color: #969896 } /* Comment */
-  .err { color: #c66 } /* Error */
-  .k { color: #b294bb } /* Keyword */
-  .l { color: #de935f } /* Literal */
-  .n { color: #c5c8c6 } /* Name */
-  .o { color: #8abeb7 } /* Operator */
-  .p { color: #c5c8c6 } /* Punctuation */
-  .cm { color: #969896 } /* Comment.Multiline */
-  .cp { color: #969896 } /* Comment.Preproc */
-  .c1 { color: #969896 } /* Comment.Single */
-  .cs { color: #969896 } /* Comment.Special */
-  .gd { color: #c66 } /* Generic.Deleted */
-  .ge { font-style: italic } /* Generic.Emph */
-  .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
-  .gi { color: #b5bd68 } /* Generic.Inserted */
-  .gp { color: #969896; font-weight: bold } /* Generic.Prompt */
-  .gs { font-weight: bold } /* Generic.Strong */
-  .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */
-  .kc { color: #b294bb } /* Keyword.Constant */
-  .kd { color: #b294bb } /* Keyword.Declaration */
-  .kn { color: #8abeb7 } /* Keyword.Namespace */
-  .kp { color: #b294bb } /* Keyword.Pseudo */
-  .kr { color: #b294bb } /* Keyword.Reserved */
-  .kt { color: #f0c674 } /* Keyword.Type */
-  .ld { color: #b5bd68 } /* Literal.Date */
-  .m { color: #de935f } /* Literal.Number */
-  .s { color: #b5bd68 } /* Literal.String */
-  .na { color: #81a2be } /* Name.Attribute */
-  .nb { color: #c5c8c6 } /* Name.Builtin */
-  .nc { color: #f0c674 } /* Name.Class */
-  .no { color: #c66 } /* Name.Constant */
-  .nd { color: #8abeb7 } /* Name.Decorator */
-  .ni { color: #c5c8c6 } /* Name.Entity */
-  .ne { color: #c66 } /* Name.Exception */
-  .nf { color: #81a2be } /* Name.Function */
-  .nl { color: #c5c8c6 } /* Name.Label */
-  .nn { color: #f0c674 } /* Name.Namespace */
-  .nx { color: #81a2be } /* Name.Other */
-  .py { color: #c5c8c6 } /* Name.Property */
-  .nt { color: #8abeb7 } /* Name.Tag */
-  .nv { color: #c66 } /* Name.Variable */
-  .ow { color: #8abeb7 } /* Operator.Word */
-  .w { color: #c5c8c6 } /* Text.Whitespace */
-  .mf { color: #de935f } /* Literal.Number.Float */
-  .mh { color: #de935f } /* Literal.Number.Hex */
-  .mi { color: #de935f } /* Literal.Number.Integer */
-  .mo { color: #de935f } /* Literal.Number.Oct */
-  .sb { color: #b5bd68 } /* Literal.String.Backtick */
-  .sc { color: #c5c8c6 } /* Literal.String.Char */
-  .sd { color: #969896 } /* Literal.String.Doc */
-  .s2 { color: #b5bd68 } /* Literal.String.Double */
-  .se { color: #de935f } /* Literal.String.Escape */
-  .sh { color: #b5bd68 } /* Literal.String.Heredoc */
-  .si { color: #de935f } /* Literal.String.Interpol */
-  .sx { color: #b5bd68 } /* Literal.String.Other */
-  .sr { color: #b5bd68 } /* Literal.String.Regex */
-  .s1 { color: #b5bd68 } /* Literal.String.Single */
-  .ss { color: #b5bd68 } /* Literal.String.Symbol */
-  .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
-  .vc { color: #c66 } /* Name.Variable.Class */
-  .vg { color: #c66 } /* Name.Variable.Global */
-  .vi { color: #c66 } /* Name.Variable.Instance */
-  .il { color: #de935f } /* Literal.Number.Integer.Long */
+  .hll { background-color: #373b41; }
+  .c { color: #969896; } /* Comment */
+  .err { color: #c66; } /* Error */
+  .k { color: #b294bb; } /* Keyword */
+  .l { color: #de935f; } /* Literal */
+  .n { color: #c5c8c6; } /* Name */
+  .o { color: #8abeb7; } /* Operator */
+  .p { color: #c5c8c6; } /* Punctuation */
+  .cm { color: #969896; } /* Comment.Multiline */
+  .cp { color: #969896; } /* Comment.Preproc */
+  .c1 { color: #969896; } /* Comment.Single */
+  .cs { color: #969896; } /* Comment.Special */
+  .gd { color: #c66; } /* Generic.Deleted */
+  .ge { font-style: italic; } /* Generic.Emph */
+  .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */
+  .gi { color: #b5bd68; } /* Generic.Inserted */
+  .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */
+  .gs { font-weight: bold; } /* Generic.Strong */
+  .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */
+  .kc { color: #b294bb; } /* Keyword.Constant */
+  .kd { color: #b294bb; } /* Keyword.Declaration */
+  .kn { color: #8abeb7; } /* Keyword.Namespace */
+  .kp { color: #b294bb; } /* Keyword.Pseudo */
+  .kr { color: #b294bb; } /* Keyword.Reserved */
+  .kt { color: #f0c674; } /* Keyword.Type */
+  .ld { color: #b5bd68; } /* Literal.Date */
+  .m { color: #de935f; } /* Literal.Number */
+  .s { color: #b5bd68; } /* Literal.String */
+  .na { color: #81a2be; } /* Name.Attribute */
+  .nb { color: #c5c8c6; } /* Name.Builtin */
+  .nc { color: #f0c674; } /* Name.Class */
+  .no { color: #c66; } /* Name.Constant */
+  .nd { color: #8abeb7; } /* Name.Decorator */
+  .ni { color: #c5c8c6; } /* Name.Entity */
+  .ne { color: #c66; } /* Name.Exception */
+  .nf { color: #81a2be; } /* Name.Function */
+  .nl { color: #c5c8c6; } /* Name.Label */
+  .nn { color: #f0c674; } /* Name.Namespace */
+  .nx { color: #81a2be; } /* Name.Other */
+  .py { color: #c5c8c6; } /* Name.Property */
+  .nt { color: #8abeb7; } /* Name.Tag */
+  .nv { color: #c66; } /* Name.Variable */
+  .ow { color: #8abeb7; } /* Operator.Word */
+  .w { color: #c5c8c6; } /* Text.Whitespace */
+  .mf { color: #de935f; } /* Literal.Number.Float */
+  .mh { color: #de935f; } /* Literal.Number.Hex */
+  .mi { color: #de935f; } /* Literal.Number.Integer */
+  .mo { color: #de935f; } /* Literal.Number.Oct */
+  .sb { color: #b5bd68; } /* Literal.String.Backtick */
+  .sc { color: #c5c8c6; } /* Literal.String.Char */
+  .sd { color: #969896; } /* Literal.String.Doc */
+  .s2 { color: #b5bd68; } /* Literal.String.Double */
+  .se { color: #de935f; } /* Literal.String.Escape */
+  .sh { color: #b5bd68; } /* Literal.String.Heredoc */
+  .si { color: #de935f; } /* Literal.String.Interpol */
+  .sx { color: #b5bd68; } /* Literal.String.Other */
+  .sr { color: #b5bd68; } /* Literal.String.Regex */
+  .s1 { color: #b5bd68; } /* Literal.String.Single */
+  .ss { color: #b5bd68; } /* Literal.String.Symbol */
+  .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */
+  .vc { color: #c66; } /* Name.Variable.Class */
+  .vg { color: #c66; } /* Name.Variable.Global */
+  .vi { color: #c66; } /* Name.Variable.Instance */
+  .il { color: #de935f; } /* Literal.Number.Integer.Long */
 }
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 80a509a7c1ac9fa3b949bf8269228d7aa4bc7b9a..db8da8aab104c56e4fc71a3bdd1b7e098ef7f350 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,43 +1,53 @@
 /* https://github.com/richleland/pygments-css/blob/master/monokai.css */
 .code.monokai {
   // Line numbers
-  .line-numbers, .diff-line-num {
+  .line-numbers,
+  .diff-line-num {
     background-color: #272822;
   }
 
-  .diff-line-num, .diff-line-num a {
+  .diff-line-num,
+  .diff-line-num a {
     color: rgba(255, 255, 255, 0.3);
   }
 
   // Code itself
-  pre.code, .diff-line-num {
+  pre.code,
+  .diff-line-num {
     border-color: #555;
   }
 
-  &, pre.code, .line_holder .line_content {
+  &,
+  pre.code,
+  .line_holder .line_content {
     background-color: #272822;
     color: #f8f8f2;
   }
 
   // Diff line
   .line_holder {
+    &.match .line_content {
+      @include dark-diff-match-line;
+    }
+
     td.diff-line-num.hll:not(.empty-cell),
     td.line_content.hll:not(.empty-cell) {
       background-color: #49483e;
       border-color: darken(#49483e, 15%);
     }
 
-    .diff-line-num.new, .line_content.new {
+    .diff-line-num.new,
+    .line_content.new {
       @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
     }
 
-    .diff-line-num.old, .line_content.old {
+    .diff-line-num.old,
+    .line_content.old {
       @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
@@ -52,65 +62,65 @@
     color: #000 !important;
   }
 
-  .hll { background-color: #49483e }
-  .c { color: #75715e } /* Comment */
-  .err { color: #960050; background-color: #1e0010 } /* Error */
-  .k { color: #66d9ef } /* Keyword */
-  .l { color: #ae81ff } /* Literal */
-  .n { color: #f8f8f2 } /* Name */
-  .o { color: #f92672 } /* Operator */
-  .p { color: #f8f8f2 } /* Punctuation */
-  .cm { color: #75715e } /* Comment.Multiline */
-  .cp { color: #75715e } /* Comment.Preproc */
-  .c1 { color: #75715e } /* Comment.Single */
-  .cs { color: #75715e } /* Comment.Special */
-  .ge { font-style: italic } /* Generic.Emph */
-  .gs { font-weight: bold } /* Generic.Strong */
-  .kc { color: #66d9ef } /* Keyword.Constant */
-  .kd { color: #66d9ef } /* Keyword.Declaration */
-  .kn { color: #f92672 } /* Keyword.Namespace */
-  .kp { color: #66d9ef } /* Keyword.Pseudo */
-  .kr { color: #66d9ef } /* Keyword.Reserved */
-  .kt { color: #66d9ef } /* Keyword.Type */
-  .ld { color: #e6db74 } /* Literal.Date */
-  .m { color: #ae81ff } /* Literal.Number */
-  .s { color: #e6db74 } /* Literal.String */
-  .na { color: #a6e22e } /* Name.Attribute */
-  .nb { color: #f8f8f2 } /* Name.Builtin */
-  .nc { color: #a6e22e } /* Name.Class */
-  .no { color: #66d9ef } /* Name.Constant */
-  .nd { color: #a6e22e } /* Name.Decorator */
-  .ni { color: #f8f8f2 } /* Name.Entity */
-  .ne { color: #a6e22e } /* Name.Exception */
-  .nf { color: #a6e22e } /* Name.Function */
-  .nl { color: #f8f8f2 } /* Name.Label */
-  .nn { color: #f8f8f2 } /* Name.Namespace */
-  .nx { color: #a6e22e } /* Name.Other */
-  .py { color: #f8f8f2 } /* Name.Property */
-  .nt { color: #f92672 } /* Name.Tag */
-  .nv { color: #f8f8f2 } /* Name.Variable */
-  .ow { color: #f92672 } /* Operator.Word */
-  .w { color: #f8f8f2 } /* Text.Whitespace */
-  .mf { color: #ae81ff } /* Literal.Number.Float */
-  .mh { color: #ae81ff } /* Literal.Number.Hex */
-  .mi { color: #ae81ff } /* Literal.Number.Integer */
-  .mo { color: #ae81ff } /* Literal.Number.Oct */
-  .sb { color: #e6db74 } /* Literal.String.Backtick */
-  .sc { color: #e6db74 } /* Literal.String.Char */
-  .sd { color: #e6db74 } /* Literal.String.Doc */
-  .s2 { color: #e6db74 } /* Literal.String.Double */
-  .se { color: #ae81ff } /* Literal.String.Escape */
-  .sh { color: #e6db74 } /* Literal.String.Heredoc */
-  .si { color: #e6db74 } /* Literal.String.Interpol */
-  .sx { color: #e6db74 } /* Literal.String.Other */
-  .sr { color: #e6db74 } /* Literal.String.Regex */
-  .s1 { color: #e6db74 } /* Literal.String.Single */
-  .ss { color: #e6db74 } /* Literal.String.Symbol */
-  .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
-  .vc { color: #f8f8f2 } /* Name.Variable.Class */
-  .vg { color: #f8f8f2 } /* Name.Variable.Global */
-  .vi { color: #f8f8f2 } /* Name.Variable.Instance */
-  .il { color: #ae81ff } /* Literal.Number.Integer.Long */
+  .hll { background-color: #49483e; }
+  .c { color: #75715e; } /* Comment */
+  .err { color: #960050; background-color: #1e0010; } /* Error */
+  .k { color: #66d9ef; } /* Keyword */
+  .l { color: #ae81ff; } /* Literal */
+  .n { color: #f8f8f2; } /* Name */
+  .o { color: #f92672; } /* Operator */
+  .p { color: #f8f8f2; } /* Punctuation */
+  .cm { color: #75715e; } /* Comment.Multiline */
+  .cp { color: #75715e; } /* Comment.Preproc */
+  .c1 { color: #75715e; } /* Comment.Single */
+  .cs { color: #75715e; } /* Comment.Special */
+  .ge { font-style: italic; } /* Generic.Emph */
+  .gs { font-weight: bold; } /* Generic.Strong */
+  .kc { color: #66d9ef; } /* Keyword.Constant */
+  .kd { color: #66d9ef; } /* Keyword.Declaration */
+  .kn { color: #f92672; } /* Keyword.Namespace */
+  .kp { color: #66d9ef; } /* Keyword.Pseudo */
+  .kr { color: #66d9ef; } /* Keyword.Reserved */
+  .kt { color: #66d9ef; } /* Keyword.Type */
+  .ld { color: #e6db74; } /* Literal.Date */
+  .m { color: #ae81ff; } /* Literal.Number */
+  .s { color: #e6db74; } /* Literal.String */
+  .na { color: #a6e22e; } /* Name.Attribute */
+  .nb { color: #f8f8f2; } /* Name.Builtin */
+  .nc { color: #a6e22e; } /* Name.Class */
+  .no { color: #66d9ef; } /* Name.Constant */
+  .nd { color: #a6e22e; } /* Name.Decorator */
+  .ni { color: #f8f8f2; } /* Name.Entity */
+  .ne { color: #a6e22e; } /* Name.Exception */
+  .nf { color: #a6e22e; } /* Name.Function */
+  .nl { color: #f8f8f2; } /* Name.Label */
+  .nn { color: #f8f8f2; } /* Name.Namespace */
+  .nx { color: #a6e22e; } /* Name.Other */
+  .py { color: #f8f8f2; } /* Name.Property */
+  .nt { color: #f92672; } /* Name.Tag */
+  .nv { color: #f8f8f2; } /* Name.Variable */
+  .ow { color: #f92672; } /* Operator.Word */
+  .w { color: #f8f8f2; } /* Text.Whitespace */
+  .mf { color: #ae81ff; } /* Literal.Number.Float */
+  .mh { color: #ae81ff; } /* Literal.Number.Hex */
+  .mi { color: #ae81ff; } /* Literal.Number.Integer */
+  .mo { color: #ae81ff; } /* Literal.Number.Oct */
+  .sb { color: #e6db74; } /* Literal.String.Backtick */
+  .sc { color: #e6db74; } /* Literal.String.Char */
+  .sd { color: #e6db74; } /* Literal.String.Doc */
+  .s2 { color: #e6db74; } /* Literal.String.Double */
+  .se { color: #ae81ff; } /* Literal.String.Escape */
+  .sh { color: #e6db74; } /* Literal.String.Heredoc */
+  .si { color: #e6db74; } /* Literal.String.Interpol */
+  .sx { color: #e6db74; } /* Literal.String.Other */
+  .sr { color: #e6db74; } /* Literal.String.Regex */
+  .s1 { color: #e6db74; } /* Literal.String.Single */
+  .ss { color: #e6db74; } /* Literal.String.Symbol */
+  .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */
+  .vc { color: #f8f8f2; } /* Name.Variable.Class */
+  .vg { color: #f8f8f2; } /* Name.Variable.Global */
+  .vi { color: #f8f8f2; } /* Name.Variable.Instance */
+  .il { color: #ae81ff; } /* Literal.Number.Integer.Long */
   .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */
   .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */
   .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index c62bd021aefda15de6fe2baea4d02b965b97e294..a87333146de1b2b38f9ff80bea7ffe11f6b6b49c 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,43 +1,53 @@
 /* https://gist.github.com/qguv/7936275 */
 .code.solarized-dark {
   // Line numbers
-  .line-numbers, .diff-line-num {
+  .line-numbers,
+  .diff-line-num {
     background-color: #002b36;
   }
 
-  .diff-line-num, .diff-line-num a {
+  .diff-line-num,
+  .diff-line-num a {
     color: rgba(255, 255, 255, 0.3);
   }
 
   // Code itself
-  pre.code, .diff-line-num {
+  pre.code,
+  .diff-line-num {
     border-color: #113b46;
   }
 
-  &, pre.code, .line_holder .line_content {
+  &,
+  pre.code,
+  .line_holder .line_content {
     background-color: #002b36;
     color: #93a1a1;
   }
 
   // Diff line
   .line_holder {
+    &.match .line_content {
+      @include dark-diff-match-line;
+    }
+
     td.diff-line-num.hll:not(.empty-cell),
     td.line_content.hll:not(.empty-cell) {
       background-color: #174652;
       border-color: darken(#174652, 15%);
     }
 
-    .diff-line-num.new, .line_content.new {
+    .diff-line-num.new,
+    .line_content.new {
       @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
     }
 
-    .diff-line-num.old, .line_content.old {
+    .diff-line-num.old,
+    .line_content.old {
       @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
     }
 
     .line_content.match {
-      color: rgba(255, 255, 255, 0.3);
-      background: rgba(255, 255, 255, 0.1);
+      @include dark-diff-match-line;
     }
   }
 
@@ -69,72 +79,72 @@
   green     #859900  operators, other keywords
   */
 
-  .c { color: #586e75 } /* Comment */
-  .err { color: #93a1a1 } /* Error */
-  .g { color: #93a1a1 } /* Generic */
-  .k { color: #859900 } /* Keyword */
-  .l { color: #93a1a1 } /* Literal */
-  .n { color: #93a1a1 } /* Name */
-  .o { color: #859900 } /* Operator */
-  .x { color: #cb4b16 } /* Other */
-  .p { color: #93a1a1 } /* Punctuation */
-  .cm { color: #586e75 } /* Comment.Multiline */
-  .cp { color: #859900 } /* Comment.Preproc */
-  .c1 { color: #586e75 } /* Comment.Single */
-  .cs { color: #859900 } /* Comment.Special */
-  .gd { color: #2aa198 } /* Generic.Deleted */
-  .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */
-  .gr { color: #dc322f } /* Generic.Error */
-  .gh { color: #cb4b16 } /* Generic.Heading */
-  .gi { color: #859900 } /* Generic.Inserted */
-  .go { color: #93a1a1 } /* Generic.Output */
-  .gp { color: #93a1a1 } /* Generic.Prompt */
-  .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */
-  .gu { color: #cb4b16 } /* Generic.Subheading */
-  .gt { color: #93a1a1 } /* Generic.Traceback */
-  .kc { color: #cb4b16 } /* Keyword.Constant */
-  .kd { color: #268bd2 } /* Keyword.Declaration */
-  .kn { color: #859900 } /* Keyword.Namespace */
-  .kp { color: #859900 } /* Keyword.Pseudo */
-  .kr { color: #268bd2 } /* Keyword.Reserved */
-  .kt { color: #dc322f } /* Keyword.Type */
-  .ld { color: #93a1a1 } /* Literal.Date */
-  .m { color: #2aa198 } /* Literal.Number */
-  .s { color: #2aa198 } /* Literal.String */
-  .na { color: #93a1a1 } /* Name.Attribute */
-  .nb { color: #b58900 } /* Name.Builtin */
-  .nc { color: #268bd2 } /* Name.Class */
-  .no { color: #cb4b16 } /* Name.Constant */
-  .nd { color: #268bd2 } /* Name.Decorator */
-  .ni { color: #cb4b16 } /* Name.Entity */
-  .ne { color: #cb4b16 } /* Name.Exception */
-  .nf { color: #268bd2 } /* Name.Function */
-  .nl { color: #93a1a1 } /* Name.Label */
-  .nn { color: #93a1a1 } /* Name.Namespace */
-  .nx { color: #93a1a1 } /* Name.Other */
-  .py { color: #93a1a1 } /* Name.Property */
-  .nt { color: #268bd2 } /* Name.Tag */
-  .nv { color: #268bd2 } /* Name.Variable */
-  .ow { color: #859900 } /* Operator.Word */
-  .w { color: #93a1a1 } /* Text.Whitespace */
-  .mf { color: #2aa198 } /* Literal.Number.Float */
-  .mh { color: #2aa198 } /* Literal.Number.Hex */
-  .mi { color: #2aa198 } /* Literal.Number.Integer */
-  .mo { color: #2aa198 } /* Literal.Number.Oct */
-  .sb { color: #586e75 } /* Literal.String.Backtick */
-  .sc { color: #2aa198 } /* Literal.String.Char */
-  .sd { color: #93a1a1 } /* Literal.String.Doc */
-  .s2 { color: #2aa198 } /* Literal.String.Double */
-  .se { color: #cb4b16 } /* Literal.String.Escape */
-  .sh { color: #93a1a1 } /* Literal.String.Heredoc */
-  .si { color: #2aa198 } /* Literal.String.Interpol */
-  .sx { color: #2aa198 } /* Literal.String.Other */
-  .sr { color: #dc322f } /* Literal.String.Regex */
-  .s1 { color: #2aa198 } /* Literal.String.Single */
-  .ss { color: #2aa198 } /* Literal.String.Symbol */
-  .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
-  .vc { color: #268bd2 } /* Name.Variable.Class */
-  .vg { color: #268bd2 } /* Name.Variable.Global */
-  .vi { color: #268bd2 } /* Name.Variable.Instance */
-  .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+  .c { color: #586e75; } /* Comment */
+  .err { color: #93a1a1; } /* Error */
+  .g { color: #93a1a1; } /* Generic */
+  .k { color: #859900; } /* Keyword */
+  .l { color: #93a1a1; } /* Literal */
+  .n { color: #93a1a1; } /* Name */
+  .o { color: #859900; } /* Operator */
+  .x { color: #cb4b16; } /* Other */
+  .p { color: #93a1a1; } /* Punctuation */
+  .cm { color: #586e75; } /* Comment.Multiline */
+  .cp { color: #859900; } /* Comment.Preproc */
+  .c1 { color: #586e75; } /* Comment.Single */
+  .cs { color: #859900; } /* Comment.Special */
+  .gd { color: #2aa198; } /* Generic.Deleted */
+  .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */
+  .gr { color: #dc322f; } /* Generic.Error */
+  .gh { color: #cb4b16; } /* Generic.Heading */
+  .gi { color: #859900; } /* Generic.Inserted */
+  .go { color: #93a1a1; } /* Generic.Output */
+  .gp { color: #93a1a1; } /* Generic.Prompt */
+  .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */
+  .gu { color: #cb4b16; } /* Generic.Subheading */
+  .gt { color: #93a1a1; } /* Generic.Traceback */
+  .kc { color: #cb4b16; } /* Keyword.Constant */
+  .kd { color: #268bd2; } /* Keyword.Declaration */
+  .kn { color: #859900; } /* Keyword.Namespace */
+  .kp { color: #859900; } /* Keyword.Pseudo */
+  .kr { color: #268bd2; } /* Keyword.Reserved */
+  .kt { color: #dc322f; } /* Keyword.Type */
+  .ld { color: #93a1a1; } /* Literal.Date */
+  .m { color: #2aa198; } /* Literal.Number */
+  .s { color: #2aa198; } /* Literal.String */
+  .na { color: #93a1a1; } /* Name.Attribute */
+  .nb { color: #b58900; } /* Name.Builtin */
+  .nc { color: #268bd2; } /* Name.Class */
+  .no { color: #cb4b16; } /* Name.Constant */
+  .nd { color: #268bd2; } /* Name.Decorator */
+  .ni { color: #cb4b16; } /* Name.Entity */
+  .ne { color: #cb4b16; } /* Name.Exception */
+  .nf { color: #268bd2; } /* Name.Function */
+  .nl { color: #93a1a1; } /* Name.Label */
+  .nn { color: #93a1a1; } /* Name.Namespace */
+  .nx { color: #93a1a1; } /* Name.Other */
+  .py { color: #93a1a1; } /* Name.Property */
+  .nt { color: #268bd2; } /* Name.Tag */
+  .nv { color: #268bd2; } /* Name.Variable */
+  .ow { color: #859900; } /* Operator.Word */
+  .w { color: #93a1a1; } /* Text.Whitespace */
+  .mf { color: #2aa198; } /* Literal.Number.Float */
+  .mh { color: #2aa198; } /* Literal.Number.Hex */
+  .mi { color: #2aa198; } /* Literal.Number.Integer */
+  .mo { color: #2aa198; } /* Literal.Number.Oct */
+  .sb { color: #586e75; } /* Literal.String.Backtick */
+  .sc { color: #2aa198; } /* Literal.String.Char */
+  .sd { color: #93a1a1; } /* Literal.String.Doc */
+  .s2 { color: #2aa198; } /* Literal.String.Double */
+  .se { color: #cb4b16; } /* Literal.String.Escape */
+  .sh { color: #93a1a1; } /* Literal.String.Heredoc */
+  .si { color: #2aa198; } /* Literal.String.Interpol */
+  .sx { color: #2aa198; } /* Literal.String.Other */
+  .sr { color: #dc322f; } /* Literal.String.Regex */
+  .s1 { color: #2aa198; } /* Literal.String.Single */
+  .ss { color: #2aa198; } /* Literal.String.Symbol */
+  .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+  .vc { color: #268bd2; } /* Name.Variable.Class */
+  .vg { color: #268bd2; } /* Name.Variable.Global */
+  .vi { color: #268bd2; } /* Name.Variable.Instance */
+  .il { color: #2aa198; } /* Literal.Number.Integer.Long */
 }
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 524cfaf90c309c43d0aac388acbaf80fc87984b3..faff353ded7348ee676f10426c7828beed91564d 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,43 +1,59 @@
 /* https://gist.github.com/qguv/7936275 */
+
+@mixin matchLine {
+  color: $black-transparent;
+  background: rgba(255, 255, 255, 0.4);
+}
+
 .code.solarized-light {
   // Line numbers
-  .line-numbers, .diff-line-num {
+  .line-numbers,
+  .diff-line-num {
     background-color: #fdf6e3;
   }
 
-  .diff-line-num, .diff-line-num a {
+  .diff-line-num,
+  .diff-line-num a {
     color: $black-transparent;
   }
 
   // Code itself
-  pre.code, .diff-line-num {
+  pre.code,
+  .diff-line-num {
     border-color: #c5d0d4;
   }
 
-  &, pre.code, .line_holder .line_content {
+  &,
+  pre.code,
+  .line_holder .line_content {
     background-color: #fdf6e3;
     color: #586e75;
   }
 
   // Diff line
   .line_holder {
+    &.match .line_content {
+      @include matchLine;
+    }
+
     td.diff-line-num.hll:not(.empty-cell),
     td.line_content.hll:not(.empty-cell) {
       background-color: #ddd8c5;
       border-color: darken(#ddd8c5, 15%);
     }
 
-    .diff-line-num.new, .line_content.new {
+    .diff-line-num.new,
+    .line_content.new {
       @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
     }
 
-    .diff-line-num.old, .line_content.old {
+    .diff-line-num.old,
+    .line_content.old {
       @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
     }
 
     .line_content.match {
-      color: $black-transparent;
-      background: rgba(255, 255, 255, 0.4);
+      @include matchLine;
     }
   }
 
@@ -69,72 +85,72 @@
   green     #859900  operators, other keywords
   */
 
-  .c { color: #93a1a1 } /* Comment */
-  .err { color: #586e75 } /* Error */
-  .g { color: #586e75 } /* Generic */
-  .k { color: #859900 } /* Keyword */
-  .l { color: #586e75 } /* Literal */
-  .n { color: #586e75 } /* Name */
-  .o { color: #859900 } /* Operator */
-  .x { color: #cb4b16 } /* Other */
-  .p { color: #586e75 } /* Punctuation */
-  .cm { color: #93a1a1 } /* Comment.Multiline */
-  .cp { color: #859900 } /* Comment.Preproc */
-  .c1 { color: #93a1a1 } /* Comment.Single */
-  .cs { color: #859900 } /* Comment.Special */
-  .gd { color: #2aa198 } /* Generic.Deleted */
-  .ge { color: #586e75; font-style: italic } /* Generic.Emph */
-  .gr { color: #dc322f } /* Generic.Error */
-  .gh { color: #cb4b16 } /* Generic.Heading */
-  .gi { color: #859900 } /* Generic.Inserted */
-  .go { color: #586e75 } /* Generic.Output */
-  .gp { color: #586e75 } /* Generic.Prompt */
-  .gs { color: #586e75; font-weight: bold } /* Generic.Strong */
-  .gu { color: #cb4b16 } /* Generic.Subheading */
-  .gt { color: #586e75 } /* Generic.Traceback */
-  .kc { color: #cb4b16 } /* Keyword.Constant */
-  .kd { color: #268bd2 } /* Keyword.Declaration */
-  .kn { color: #859900 } /* Keyword.Namespace */
-  .kp { color: #859900 } /* Keyword.Pseudo */
-  .kr { color: #268bd2 } /* Keyword.Reserved */
-  .kt { color: #dc322f } /* Keyword.Type */
-  .ld { color: #586e75 } /* Literal.Date */
-  .m { color: #2aa198 } /* Literal.Number */
-  .s { color: #2aa198 } /* Literal.String */
-  .na { color: #586e75 } /* Name.Attribute */
-  .nb { color: #b58900 } /* Name.Builtin */
-  .nc { color: #268bd2 } /* Name.Class */
-  .no { color: #cb4b16 } /* Name.Constant */
-  .nd { color: #268bd2 } /* Name.Decorator */
-  .ni { color: #cb4b16 } /* Name.Entity */
-  .ne { color: #cb4b16 } /* Name.Exception */
-  .nf { color: #268bd2 } /* Name.Function */
-  .nl { color: #586e75 } /* Name.Label */
-  .nn { color: #586e75 } /* Name.Namespace */
-  .nx { color: #586e75 } /* Name.Other */
-  .py { color: #586e75 } /* Name.Property */
-  .nt { color: #268bd2 } /* Name.Tag */
-  .nv { color: #268bd2 } /* Name.Variable */
-  .ow { color: #859900 } /* Operator.Word */
-  .w { color: #586e75 } /* Text.Whitespace */
-  .mf { color: #2aa198 } /* Literal.Number.Float */
-  .mh { color: #2aa198 } /* Literal.Number.Hex */
-  .mi { color: #2aa198 } /* Literal.Number.Integer */
-  .mo { color: #2aa198 } /* Literal.Number.Oct */
-  .sb { color: #93a1a1 } /* Literal.String.Backtick */
-  .sc { color: #2aa198 } /* Literal.String.Char */
-  .sd { color: #586e75 } /* Literal.String.Doc */
-  .s2 { color: #2aa198 } /* Literal.String.Double */
-  .se { color: #cb4b16 } /* Literal.String.Escape */
-  .sh { color: #586e75 } /* Literal.String.Heredoc */
-  .si { color: #2aa198 } /* Literal.String.Interpol */
-  .sx { color: #2aa198 } /* Literal.String.Other */
-  .sr { color: #dc322f } /* Literal.String.Regex */
-  .s1 { color: #2aa198 } /* Literal.String.Single */
-  .ss { color: #2aa198 } /* Literal.String.Symbol */
-  .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
-  .vc { color: #268bd2 } /* Name.Variable.Class */
-  .vg { color: #268bd2 } /* Name.Variable.Global */
-  .vi { color: #268bd2 } /* Name.Variable.Instance */
-  .il { color: #2aa198 } /* Literal.Number.Integer.Long */
+  .c { color: #93a1a1; } /* Comment */
+  .err { color: #586e75; } /* Error */
+  .g { color: #586e75; } /* Generic */
+  .k { color: #859900; } /* Keyword */
+  .l { color: #586e75; } /* Literal */
+  .n { color: #586e75; } /* Name */
+  .o { color: #859900; } /* Operator */
+  .x { color: #cb4b16; } /* Other */
+  .p { color: #586e75; } /* Punctuation */
+  .cm { color: #93a1a1; } /* Comment.Multiline */
+  .cp { color: #859900; } /* Comment.Preproc */
+  .c1 { color: #93a1a1; } /* Comment.Single */
+  .cs { color: #859900; } /* Comment.Special */
+  .gd { color: #2aa198; } /* Generic.Deleted */
+  .ge { color: #586e75; font-style: italic; } /* Generic.Emph */
+  .gr { color: #dc322f; } /* Generic.Error */
+  .gh { color: #cb4b16; } /* Generic.Heading */
+  .gi { color: #859900; } /* Generic.Inserted */
+  .go { color: #586e75; } /* Generic.Output */
+  .gp { color: #586e75; } /* Generic.Prompt */
+  .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */
+  .gu { color: #cb4b16; } /* Generic.Subheading */
+  .gt { color: #586e75; } /* Generic.Traceback */
+  .kc { color: #cb4b16; } /* Keyword.Constant */
+  .kd { color: #268bd2; } /* Keyword.Declaration */
+  .kn { color: #859900; } /* Keyword.Namespace */
+  .kp { color: #859900; } /* Keyword.Pseudo */
+  .kr { color: #268bd2; } /* Keyword.Reserved */
+  .kt { color: #dc322f; } /* Keyword.Type */
+  .ld { color: #586e75; } /* Literal.Date */
+  .m { color: #2aa198; } /* Literal.Number */
+  .s { color: #2aa198; } /* Literal.String */
+  .na { color: #586e75; } /* Name.Attribute */
+  .nb { color: #b58900; } /* Name.Builtin */
+  .nc { color: #268bd2; } /* Name.Class */
+  .no { color: #cb4b16; } /* Name.Constant */
+  .nd { color: #268bd2; } /* Name.Decorator */
+  .ni { color: #cb4b16; } /* Name.Entity */
+  .ne { color: #cb4b16; } /* Name.Exception */
+  .nf { color: #268bd2; } /* Name.Function */
+  .nl { color: #586e75; } /* Name.Label */
+  .nn { color: #586e75; } /* Name.Namespace */
+  .nx { color: #586e75; } /* Name.Other */
+  .py { color: #586e75; } /* Name.Property */
+  .nt { color: #268bd2; } /* Name.Tag */
+  .nv { color: #268bd2; } /* Name.Variable */
+  .ow { color: #859900; } /* Operator.Word */
+  .w { color: #586e75; } /* Text.Whitespace */
+  .mf { color: #2aa198; } /* Literal.Number.Float */
+  .mh { color: #2aa198; } /* Literal.Number.Hex */
+  .mi { color: #2aa198; } /* Literal.Number.Integer */
+  .mo { color: #2aa198; } /* Literal.Number.Oct */
+  .sb { color: #93a1a1; } /* Literal.String.Backtick */
+  .sc { color: #2aa198; } /* Literal.String.Char */
+  .sd { color: #586e75; } /* Literal.String.Doc */
+  .s2 { color: #2aa198; } /* Literal.String.Double */
+  .se { color: #cb4b16; } /* Literal.String.Escape */
+  .sh { color: #586e75; } /* Literal.String.Heredoc */
+  .si { color: #2aa198; } /* Literal.String.Interpol */
+  .sx { color: #2aa198; } /* Literal.String.Other */
+  .sr { color: #dc322f; } /* Literal.String.Regex */
+  .s1 { color: #2aa198; } /* Literal.String.Single */
+  .ss { color: #2aa198; } /* Literal.String.Symbol */
+  .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
+  .vc { color: #268bd2; } /* Name.Variable.Class */
+  .vg { color: #268bd2; } /* Name.Variable.Global */
+  .vi { color: #268bd2; } /* Name.Variable.Instance */
+  .il { color: #2aa198; } /* Literal.Number.Integer.Long */
 }
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 31a4e3deaac866c8446a9ef2b55c72c547a03418..d5367d5f3f01fd0a60bbb6d4b4a4af75815c8487 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,20 +1,31 @@
 /* https://github.com/aahan/pygments-github-style */
+
+@mixin matchLine {
+  color: $black-transparent;
+  background-color: $match-line;
+}
+
 .code.white {
   // Line numbers
-  .line-numbers, .diff-line-num {
+  .line-numbers,
+  .diff-line-num {
     background-color: $background-color;
   }
 
-  .diff-line-num, .diff-line-num a {
+  .diff-line-num,
+  .diff-line-num a {
     color: $black-transparent;
   }
 
   // Code itself
-  pre.code, .diff-line-num {
+  pre.code,
+  .diff-line-num {
     border-color: $table-border-gray;
   }
 
-  &, pre.code, .line_holder .line_content {
+  &,
+  pre.code,
+  .line_holder .line_content {
     background-color: #fff;
     color: #333;
   }
@@ -22,6 +33,10 @@
   // Diff line
   .line_holder {
 
+    &.match .line_content {
+      @include matchLine;
+    }
+
     .diff-line-num {
       &.old {
         background-color: $line-number-old;
@@ -57,8 +72,7 @@
       }
 
       &.match {
-        color: $black-transparent;
-        background-color: $match-line;
+        @include matchLine;
       }
 
       &.hll:not(.empty-cell) {
@@ -77,7 +91,7 @@
     background-color: #fafe3d !important;
   }
 
-  .hll { background-color: #f8f8f8 }
+  .hll { background-color: #f8f8f8; }
   .c { color: #998; font-style: italic; }
   .err { color: #a61717; background-color: #e3d2d2; }
   .k { font-weight: bold; }
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index 9495c5b3f37cb402ff9039a9f26132ccc6429a1b..b2bce482fdec566d84233f1b7f54c4b29e41c977 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -5,13 +5,13 @@
 // Styles defined here are embedded directly into the resulting email HTML via
 // the `premailer` gem.
 
-$body-background-color:    #363636;
+$body-background-color: #363636;
 $message-background-color: #fafafa;
 
-$header-color:             #6b4fbb;
-$body-color:               #444;
-$cta-color:                #e14329;
-$footer-link-color:        #7e7e7e;
+$header-color: #6b4fbb;
+$body-color: #444;
+$cta-color: #e14329;
+$footer-link-color: #7e7e7e;
 
 $font-family: Helvetica, Arial, sans-serif;
 
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 33aedf1f7c101c002fbf04d661ca35a75f285582..8d1a6020ca4a4eafd32f4dd9eee81713119faa51 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -45,7 +45,6 @@
 .line_content {
   padding-left: 0.5em;
   padding-right: 0.5em;
-  white-space: pre;
 
   &.old {
     background-color: $line-removed;
@@ -71,11 +70,15 @@
   }
 }
 
+pre {
+  margin: 0;
+}
+
 span.highlight_word {
   background-color: #fafe3d !important;
 }
 
-.hll { background-color: #f8f8f8 }
+.hll { background-color: #f8f8f8; }
 .c { color: #998; font-style: italic; }
 .err { color: #a61717; background-color: #e3d2d2; }
 .k { font-weight: bold; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index fc12964872d2ebfca0f210dc37a6817f0a4e89cc..ced8c4a99075160aa26a87b58a46d4002fee4e76 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -2,22 +2,28 @@ img {
   max-width: 100%;
   height: auto;
 }
+
 p.details {
   font-style: italic;
-  color: #777
+  color: #777;
 }
+
 .footer > p {
   font-size: small;
-  color: #777
+  color: #777;
 }
+
 pre.commit-message {
   white-space: pre-wrap;
 }
+
 .file-stats > a {
   text-decoration: none;
+
   > .new-file {
     color: #090;
   }
+
   > .deleted-file {
     color: #b00;
   }
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 5607239d92df1805797b42672cee1bf203ece5ad..6cefafd8fc7c95d87584cf6d4dc1339809902ac9 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -22,7 +22,7 @@
 
 .admin-filter form {
   .select2-container {
-    width: 100%
+    width: 100%;
   }
 
   .controls {
@@ -31,7 +31,7 @@
 
   .form-actions {
     padding-left: 130px;
-    background: #fff
+    background: #fff;
    }
 
   .visibility-levels {
@@ -56,7 +56,8 @@
   padding: 10px;
   text-align: center;
 
-  > div, p {
+  > div,
+  p {
     display: inline;
     margin: 0;
 
@@ -72,7 +73,6 @@
   margin-bottom: 20px;
 }
 
-
 // Users List
 
 .users-list {
@@ -80,10 +80,13 @@
     display: -webkit-flex;
     display: -ms-flexbox;
     display: flex;
+    white-space: nowrap;
   }
 
   .user-details {
     flex: 1 1 auto;
+    overflow: hidden;
+    padding-right: 8px;
   }
 
   .user-name {
@@ -91,10 +94,69 @@
     font-weight: 600;
   }
 
+  .user-name,
+  .user-email {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
   .dropdown {
     .btn-block {
       margin-bottom: 0;
       line-height: inherit;
     }
   }
+
+  .label-default {
+    color: $btn-transparent-color;
+  }
+}
+
+.abuse-reports {
+  .table {
+    table-layout: fixed;
+  }
+
+  .subheading {
+    padding-bottom: $gl-padding;
+  }
+
+  .message {
+    word-wrap: break-word;
+  }
+
+  .btn {
+    white-space: normal;
+    padding: $gl-btn-padding;
+  }
+
+  th {
+    width: 15%;
+
+    &.wide {
+      width: 55%;
+    }
+  }
+
+  @media (max-width: $screen-sm-max) {
+    th {
+      width: 100%;
+    }
+
+    td {
+      width: 100%;
+      float: left;
+    }
+  }
+
+  .no-reports {
+    .emoji-icon {
+      margin-left: $btn-side-margin;
+      margin-top: 3px;
+    }
+
+    span {
+      font-size: 19px;
+    }
+  }
 }
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 5faedfedd660fe8bcafab778184a3dd81b260a30..486ad16ea263aaf88992cc0aae8f45e55974035d 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,7 +1,7 @@
 .awards {
   .emoji-icon {
-    width: 20px;
-    height: 20px;
+    width: 19px;
+    height: 19px;
   }
 }
 
@@ -93,11 +93,8 @@
 }
 
 .award-control {
-  margin-right: 5px;
-  margin-bottom: 5px;
-  padding-left: 5px;
-  padding-right: 5px;
-  line-height: 20px;
+  margin: 3px 5px 3px 0;
+  padding: 5px 6px;
   outline: 0;
 
   &:hover,
@@ -130,7 +127,7 @@
   .award-control-icon {
     float: left;
     margin-right: 5px;
-    font-size: 20px;
+    font-size: 19px;
   }
 
   .award-control-icon-loading {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
new file mode 100644
index 0000000000000000000000000000000000000000..47a7e84b5c600f2ef077921dfcb731cb4a5b2476
--- /dev/null
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -0,0 +1,331 @@
+[v-cloak] {
+  display: none;
+}
+
+.user-can-drag {
+  cursor: -webkit-grab;
+  cursor: grab;
+}
+
+.is-dragging {
+  // Important because plugin sets inline CSS
+  opacity: 1!important;
+
+  * {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    // !important to make sure no style can override this when dragging
+    cursor: -webkit-grabbing!important;
+    cursor: grabbing!important;
+  }
+}
+
+.is-ghost {
+  opacity: 0.3;
+}
+
+.dropdown-menu-issues-board-new {
+  width: 320px;
+
+  .dropdown-content {
+    max-height: 150px;
+	}
+}
+
+.issue-board-dropdown-content {
+  margin: 0 8px 10px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid $dropdown-divider-color;
+
+  > p {
+    margin: 0;
+    font-size: 14px;
+  }
+}
+
+.issue-boards-page {
+  .page-with-sidebar {
+    padding-bottom: 0;
+  }
+}
+
+.boards-app {
+  position: relative;
+}
+
+.boards-app-loading {
+  width: 100%;
+  font-size: 34px;
+}
+
+.boards-list {
+  height: calc(100vh - 152px);
+  width: 100%;
+  padding-top: 25px;
+  padding-bottom: 25px;
+  padding-right: ($gl-padding / 2);
+  padding-left: ($gl-padding / 2);
+  overflow-x: scroll;
+  white-space: nowrap;
+
+  @media (min-width: $screen-sm-min) {
+    height: 475px; // Needed for PhantomJS
+    height: calc(100vh - 220px);
+    min-height: 475px;
+
+    &.is-compact {
+      width: calc(100% - 290px);
+    }
+  }
+}
+
+.board {
+  display: inline-block;
+  width: calc(85vw - 15px);
+  height: 100%;
+  padding-right: ($gl-padding / 2);
+  padding-left: ($gl-padding / 2);
+  white-space: normal;
+  vertical-align: top;
+
+  @media (min-width: $screen-sm-min) {
+    width: 400px;
+  }
+}
+
+.board-inner {
+  height: 100%;
+  font-size: $issue-boards-font-size;
+  background: $background-color;
+  border: 1px solid $border-color;
+  border-radius: $border-radius-default;
+}
+
+.board-header {
+  border-top-left-radius: $border-radius-default;
+  border-top-right-radius: $border-radius-default;
+
+  &.has-border {
+    border-top: 3px solid;
+
+    .board-title {
+      padding-top: ($gl-padding - 3px);
+    }
+  }
+}
+
+.board-inner-container {
+  border-bottom: 1px solid $border-color;
+  padding: $gl-padding;
+}
+
+.board-title {
+  position: relative;
+  margin: 0;
+  padding: $gl-padding;
+  font-size: 1em;
+  border-bottom: 1px solid $border-color;
+}
+
+.board-delete {
+  margin-right: 10px;
+  padding: 0;
+  color: $gray-darkest;
+  background-color: transparent;
+  border: 0;
+  outline: 0;
+
+  &:hover {
+    color: $gl-link-color;
+  }
+}
+
+.board-blank-state {
+  height: calc(100% - 49px);
+  padding: $gl-padding;
+  background-color: #fff;
+}
+
+.board-blank-state-list {
+  list-style: none;
+
+  > li:not(:last-child) {
+    margin-bottom: 8px;
+  }
+
+  .label-color {
+    position: relative;
+    top: 2px;
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    margin-right: 3px;
+    border-radius: $border-radius-default;
+  }
+}
+
+.board-list {
+  height: calc(100% - 49px);
+  margin-bottom: 0;
+  padding: 5px;
+  list-style: none;
+  overflow-y: scroll;
+  overflow-x: hidden;
+
+  &.is-smaller {
+    height: calc(100% - 185px);
+  }
+}
+
+.board-list-loading {
+  margin-top: 10px;
+  font-size: (26px / $issue-boards-font-size) * 1em;
+}
+
+.card {
+  position: relative;
+  padding: 10px $gl-padding;
+  background: #fff;
+  border-radius: $border-radius-default;
+  box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+  list-style: none;
+
+  &:not(:last-child) {
+    margin-bottom: 5px;
+  }
+
+  &.is-active {
+    background-color: $row-hover;
+  }
+
+  .label {
+    border: 0;
+    outline: 0;
+  }
+
+  .confidential-icon {
+    margin-right: 5px;
+  }
+}
+
+.card-title {
+  margin: 0;
+  font-size: 1em;
+
+  a {
+    color: inherit;
+    word-wrap: break-word;
+  }
+}
+
+.card-footer {
+  margin-top: 5px;
+  line-height: 25px;
+
+  .label {
+    margin-right: 5px;
+    font-size: (14px / $issue-boards-font-size) * 1em;
+  }
+
+  .avatar {
+    margin-left: 0;
+  }
+}
+
+.card-number {
+  margin-right: 5px;
+}
+
+.issue-boards-search {
+  width: 335px;
+
+  .form-control {
+    display: inline-block;
+    width: 210px;
+  }
+}
+
+.board-list-count {
+  padding: 10px 0;
+  color: $gl-placeholder-color;
+  font-size: 13px;
+
+  > .fa {
+    margin-right: 5px;
+  }
+}
+
+.board-new-issue-form {
+  margin: 5px;
+}
+
+.board-issue-count-holder {
+  margin-top: -3px;
+
+  .btn {
+    line-height: 12px;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+}
+
+.board-issue-count {
+  padding-right: 10px;
+  padding-left: 10px;
+  line-height: 21px;
+  border-radius: $border-radius-base;
+  border: 1px solid $border-color;
+
+  &.has-btn {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-width: 1px 0 1px 1px;
+  }
+}
+
+.issue-boards-sidebar {
+  &.right-sidebar {
+    top: 153px;
+    bottom: 0;
+
+    @media (min-width: $screen-sm-min) {
+      top: 220px;
+    }
+  }
+
+  .issuable-sidebar-header {
+    position: relative;
+  }
+
+  .gutter-toggle {
+    position: absolute;
+    top: 0;
+    bottom: 15px;
+    right: 0;
+    width: 22px;
+    color: $gray-darkest;
+
+    svg {
+      position: absolute;
+      top: 50%;
+      margin-top: (-11px / 2);
+    }
+
+    &:hover {
+      path {
+        fill: $gray-darkest;
+      }
+    }
+  }
+
+  .issuable-header-text {
+    width: 100%;
+    padding-right: 35px;
+
+    > strong {
+      font-weight: 600;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e26f8f7080d7ae94f7abf91ccac6c63dbb9f783a..f1d311cabbe531d21519ddb87a3bede6338479ba 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -14,18 +14,10 @@
     }
   }
 
-  .autoscroll-container {
-    position: fixed;
-    bottom: 20px;
-    right: 20px;
-    z-index: 100;
-  }
-
   .scroll-controls {
-    &.affix-top {
-      position: absolute;
-      top: 10px;
-      right: 25px;
+    .scroll-step {
+      width: 31px;
+      margin: 0 0 0 auto;
     }
 
     &.affix-bottom {
@@ -34,12 +26,13 @@
     }
 
     &.affix {
-      right: 30px;
+      right: 25px;
       bottom: 15px;
+      z-index: 1;
+    }
 
-      @media (min-width: $screen-md-min) {
-        right: 26%;
-      }
+    &.sidebar-expanded {
+      right: #{$gutter_width + ($gl-padding * 2)};
     }
 
     a {
@@ -47,28 +40,29 @@
       margin-bottom: 10px;
     }
   }
+}
 
-  .page-sidebar-collapsed {
-    .scroll-controls {
-      left: 70px;
-    }
+.build-header {
+  position: relative;
+  padding: 0;
+  display: flex;
+  min-height: 58px;
+  align-items: center;
+
+  .btn-inverted {
+    @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
   }
 
-  .nav-links {
-    svg {
-      position: relative;
-      top: 2px;
-      margin-right: 3px;
+  @media (max-width: $screen-sm-max) {
+    padding-right: 40px;
+
+    .btn-inverted {
+      display: none;
     }
   }
-}
 
-.build-header {
-  position: relative;
-  padding-right: 40px;
-
-  @media (min-width: $screen-sm-min) {
-    padding-right: 0;
+  .header-content {
+    flex: 1;
   }
 
   a {
@@ -108,28 +102,148 @@
 }
 
 .right-sidebar.build-sidebar {
-  padding-top: $gl-padding;
-  padding-bottom: $gl-padding;
+  padding: $gl-padding 0;
 
   &.right-sidebar-collapsed {
     display: none;
   }
 
+  .blocks-container {
+    padding: 0 $gl-padding;
+  }
+
   .block {
     width: 100%;
+
+    &.coverage {
+      padding: 0 16px 11px;
+    }
+
+    .btn-group-justified {
+      margin-top: 5px;
+    }
+  }
+
+  .js-build-variable {
+    color: $code-color;
+  }
+
+  .js-build-value {
+    padding: 2px 4px;
+    color: $black;
+    background-color: $white-light;
   }
 
   .build-sidebar-header {
-    padding-top: 0;
+    padding: 0 $gl-padding $gl-padding;
 
     .gutter-toggle {
       margin-top: 0;
     }
   }
+
+  .retry-link {
+    color: $gl-link-color;
+    display: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    @media (max-width: $screen-sm-max) {
+      display: block;
+    }
+  }
+
+  .stage-item {
+    cursor: pointer;
+
+    &:hover {
+      color: $gl-text-color;
+    }
+  }
+
+  .build-dropdown {
+    padding: $gl-padding 0;
+
+    .dropdown-menu-toggle {
+      margin-top: 8px;
+    }
+
+    .dropdown-menu {
+      right: $gl-padding;
+      left: $gl-padding;
+      width: auto;
+    }
+  }
+
+  .builds-container {
+    background-color: $white-light;
+    border-top: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+    max-height: 300px;
+    overflow: auto;
+
+    svg {
+      position: relative;
+      top: 2px;
+      margin-right: 3px;
+      height: 13px;
+    }
+
+    a {
+      display: block;
+      padding: $gl-padding 10px $gl-padding 40px;
+      width: 270px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+
+      &:hover {
+        color: $gl-text-color;
+      }
+    }
+
+    .build-job {
+      position: relative;
+
+      .fa-arrow-right {
+        position: absolute;
+        left: 15px;
+        top: 20px;
+        display: none;
+      }
+
+      &.active {
+        font-weight: bold;
+
+        .fa-arrow-right {
+          display: block;
+        }
+      }
+
+      &.retried {
+        background-color: $gray-lightest;
+      }
+
+      &:hover {
+        background-color: $row-hover;
+      }
+
+      .fa-refresh {
+        font-size: 13px;
+        margin-left: 3px;
+      }
+    }
+  }
 }
 
 .build-detail-row {
   margin-bottom: 5px;
+
+  &:last-of-type {
+    margin-bottom: 0;
+  }
 }
 
 .build-light-text {
@@ -142,3 +256,9 @@
   right: 0;
   margin-top: -17px;
 }
+
+@media (min-width: $screen-md-min) {
+  .sub-nav.build {
+    width: calc(100% + #{$gutter_width});
+  }
+}
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 67a9d7d2cf770f9bfd5756817be7567399007a3e..87c453a7a27cdf662b643f7216e002939326bb04 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -12,7 +12,8 @@
       border-color: $border-color;
     }
 
-    th, td {
+    th,
+    td {
       padding: 10px $gl-padding;
     }
 
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index bbe0c6c5f1fa1842bcf0528c402ecb3701c80a90..47d3e72679bb437111033fd2c430459a8c788dfb 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,14 +2,16 @@
   display: block;
 }
 
-.commit-author, .commit-committer {
+.commit-author,
+.commit-committer {
   display: block;
   color: #999;
   font-weight: normal;
   font-style: italic;
 }
 
-.commit-author strong, .commit-committer strong {
+.commit-author strong,
+.commit-committer strong {
   font-weight: bold;
   font-style: normal;
 }
@@ -31,14 +33,45 @@
 
   &.commit-info-row-header {
     line-height: 34px;
+    padding: 10px 0;
+    margin-bottom: 0;
 
     @media (min-width: $screen-sm-min) {
-      margin-bottom: 0;
+      display: flex;
+      align-items: center;
+
+      .commit-meta {
+        flex: 1;
+      }
     }
 
-    .commit-options-dropdown-caret {
-      @media (max-width: $screen-sm) {
-        margin-left: 0;
+    .commit-hash-full {
+      @media (max-width: $screen-sm-max) {
+        width: 80px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: inline-block;
+        vertical-align: bottom;
+      }
+    }
+
+    .commit-action-buttons {
+      i {
+        color: $gl-icon-color;
+        font-size: 13px;
+        margin-right: 3px;
+      }
+
+      @media (max-width: $screen-xs-max) {
+        .dropdown {
+          width: 100%;
+          margin-top: 10px;
+        }
+
+        .dropdown-toggle {
+          width: 100%;
+        }
       }
     }
   }
@@ -51,6 +84,7 @@
       margin-left: 4px;
     }
   }
+
   .commit-committer-link,
   .commit-author-link {
     color: $gl-gray;
@@ -66,6 +100,67 @@
       margin-left: 8px;
     }
   }
+
+  .ci-status-link {
+
+    svg {
+      position: relative;
+      top: 2px;
+      margin: 0 2px 0 3px;
+    }
+  }
+}
+
+.js-details-expand {
+  &:hover {
+    text-decoration: none;
+  }
+}
+
+.commit-info-widget {
+  background: $background-color;
+  color: $gl-gray;
+  border: 1px solid $border-color;
+  border-radius: $border-radius-default;
+
+  .widget-row {
+    padding: $gl-padding;
+
+    &:not(:last-of-type) {
+      border-bottom: 1px solid $widget-inner-border;
+    }
+
+    &.branch-info {
+      .monospace,
+      .commit-info {
+        margin-left: 4px;
+      }
+    }
+  }
+
+  .icon-container {
+    display: inline-block;
+    margin-right: 8px;
+
+    svg {
+      position: relative;
+      top: 2px;
+      height: 16px;
+      width: 16px;
+    }
+
+    &.commit-icon {
+      svg {
+        path {
+          fill: $gl-text-color;
+        }
+      }
+    }
+  }
+
+  .label.label-gray {
+    background-color: $widget-expand-item;
+  }
 }
 
 .ci-status-link {
@@ -76,6 +171,7 @@
 
 .commit-box {
   border-top: 1px solid $border-color;
+  padding: $gl-padding 0;
 
   .commit-title {
     margin: 0;
@@ -99,21 +195,25 @@
       line-height: 20px;
     }
   }
+
   .new-file {
     a {
       color: $gl-text-green;
     }
   }
+
   .renamed-file {
     a {
       color: $gl-text-orange;
     }
   }
+
   .deleted-file {
     a {
       color: $gl-text-red;
     }
   }
+
   .edit-file {
     a {
       color: $gl-text-color;
@@ -121,14 +221,6 @@
   }
 }
 
-.commit-action-buttons {
-  i {
-    color: $gl-icon-color;
-    font-size: 13px;
-    margin-right: 3px;
-  }
-}
-
 /*
  * Commit message textarea for web editor and
  * custom merge request message
@@ -149,6 +241,7 @@
     position: absolute;
     z-index: 1;
   }
+
   > textarea {
     background-color: rgba(0, 0, 0, 0.0);
     font-family: inherit;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 6a58b445afaf580328f8c2b85a7b7b82d136ad56..98a84351a3d2bf2f6629adbe1bca33718c1318ef 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -18,8 +18,7 @@
 }
 
 .commit-row-title {
-  line-height: 1;
-  margin-bottom: 7px;
+  line-height: 1.35;
 
   .notes_count {
     float: right;
@@ -34,20 +33,22 @@
     color: $gl-dark-link-color;
   }
 
-  .text-expander {
-    display: inline-block;
-    background: $gray-light;
-    color: $gl-placeholder-color;
-    padding: 0 5px;
-    cursor: pointer;
-    border: 1px solid $border-gray-dark;
-    border-radius: $border-radius-default;
-    margin-left: 5px;
-
-    &:hover {
-      background-color: darken($gray-light, 10%);
-      text-decoration: none;
-    }
+}
+
+.text-expander {
+  display: inline-block;
+  background: $gray-light;
+  color: $gl-placeholder-color;
+  padding: 0 5px;
+  cursor: pointer;
+  border: 1px solid $border-gray-dark;
+  border-radius: $border-radius-default;
+  margin-left: 5px;
+  line-height: 1;
+
+  &:hover {
+    background-color: darken($gray-light, 10%);
+    text-decoration: none;
   }
 }
 
@@ -63,7 +64,8 @@
     display: inline-block;
   }
 
-  .btn-clipboard, .btn-transparent {
+  .btn-clipboard,
+  .btn-transparent {
     padding-left: 0;
     padding-right: 0;
   }
@@ -113,11 +115,13 @@
 
   .commit-row-description {
     font-size: 14px;
-    border-left: 1px solid #eee;
+    border-left: 1px solid $btn-gray-hover;
     padding: 10px 15px;
     margin: 10px 0;
-    background: #f9f9f9;
+    background: $gray-light;
     display: none;
+    white-space: pre-line;
+    word-break: normal;
 
     pre {
       border: none;
@@ -134,7 +138,7 @@
 
   .commit-row-info {
     color: $gl-gray;
-    line-height: 1;
+    line-height: 1.35;
 
     a {
       color: $gl-gray;
@@ -159,7 +163,24 @@
 
 .branch-commit {
   color: $gl-gray;
-  .commit-id, .commit-row-message {
+
+  .commit-icon {
+    text-align: center;
+    display: inline-block;
+
+    svg {
+      height: 14px;
+      width: 14px;
+      vertical-align: middle;
+      fill: $table-text-gray;
+    }
+  }
+
+  .commit-id {
+    color: $gl-link-color;
+  }
+
+  .commit-row-message {
     color: $gl-gray;
   }
 }
diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss
index 292225c52617979b1e07174999f99fe57a77cd22..81e5cee240d6084b094f8cca6c9b86edfae6767f 100644
--- a/app/assets/stylesheets/pages/confirmation.scss
+++ b/app/assets/stylesheets/pages/confirmation.scss
@@ -2,7 +2,12 @@
   margin-bottom: 20px;
   border-bottom: 1px solid #eee;
 
-  > h1, h2, h3, h4, h5, h6 {
+  > h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
     font-weight: 400;
   }
 
@@ -10,7 +15,8 @@
     margin-bottom: 20px;
   }
 
-  ul, ol {
+  ul,
+  ol {
     padding-left: 0;
   }
 
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
new file mode 100644
index 0000000000000000000000000000000000000000..572e1e7d558c4c7e65ccc99d14b90a1c26b2e541
--- /dev/null
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -0,0 +1,144 @@
+#cycle-analytics {
+  margin: 24px auto 0;
+  max-width: 800px;
+  position: relative;
+
+  .panel {
+
+    .content-block {
+      padding: 24px 0;
+      border-bottom: none;
+      position: relative;
+
+      @media (max-width: $screen-sm-min) {
+        padding: 6px 0 24px;
+      }
+    }
+
+    .column {
+      text-align: center;
+
+      @media (max-width: $screen-sm-min) {
+        padding: 15px 0;
+      }
+
+      .header {
+        font-size: 30px;
+        line-height: 38px;
+        font-weight: normal;
+        margin: 0;
+      }
+
+      .text {
+        color: $layout-link-gray;
+        margin: 0;
+      }
+
+      &:last-child {
+        text-align: right;
+
+        @media (max-width: $screen-sm-min) {
+          text-align: center;
+        }
+      }
+    }
+
+    .dropdown {
+      top: 13px;
+    }
+  }
+
+  .bordered-box {
+    border: 1px solid $border-color;
+    border-radius: $border-radius-default;
+
+  }
+
+  .content-list {
+    li {
+      padding: 18px $gl-padding $gl-padding;
+
+      .container-fluid {
+        padding: 0;
+      }
+    }
+
+    .title-col {
+      p {
+        margin: 0;
+
+        &.title {
+          line-height: 19px;
+          font-size: 15px;
+          font-weight: 600;
+          color: $gl-title-color;
+        }
+
+        &.text {
+          color: $layout-link-gray;
+
+          &.value-col {
+            color: $gl-title-color;
+          }
+        }
+      }
+    }
+
+    .value-col {
+      text-align: right;
+
+      span {
+        position: relative;
+        vertical-align: middle;
+        top: 3px;
+      }
+    }
+  }
+
+  .landing {
+    margin-bottom: $gl-padding;
+    overflow: hidden;
+
+    .dismiss-icon {
+      position: absolute;
+      right: $cycle-analytics-box-padding;
+      cursor: pointer;
+      color: #b2b2b2;
+    }
+
+    .svg-container {
+      text-align: center;
+
+      svg {
+        width: 136px;
+        height: 136px;
+      }
+    }
+
+    .inner-content {
+      @media (max-width: $screen-sm-min) {
+        padding: 0 28px;
+        text-align: center;
+      }
+
+      h4 {
+        color: $gl-text-color;
+        font-size: 17px;
+      }
+
+      p {
+        color: $cycle-analytics-box-text-color;
+        margin-bottom: $gl-padding;
+      }
+    }
+  }
+
+  .fa-spinner {
+    font-size: 28px;
+    position: relative;
+    margin-left: -20px;
+    left: 50%;
+    margin-top: 36px;
+  }
+
+}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 42928ee279c252c15401dc91f83eb08344ec0ceb..016bab104eb2b933e480d6f8b00aec7a16532bfd 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -5,6 +5,7 @@
         background: $background-color;
         border-top-left-radius: 0;
       }
+
       border-top-left-radius: 0;
     }
   }
@@ -17,6 +18,7 @@
     float: left;
     @extend .col-md-2;
   }
+
   .btn {
     margin-left: 5px;
     float: left;
@@ -34,10 +36,6 @@
   }
 }
 
-.dash-project-avatar {
-  float: left;
-}
-
 .dash-project-access-icon {
   float: left;
   margin-right: 5px;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 1b389d83525da3cd5e2bfcad042bd1cda135218d..0f0c0abe7ae8830b036461d7e3b39aaa9ce56abd 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -13,16 +13,19 @@
     color: #5c5d5e;
   }
 
-  .issue_created_ago, .author_link {
+  .issue_created_ago,
+  .author_link {
     white-space: nowrap;
   }
 }
 
 .detail-page-description {
   .title {
-    margin: 0;
-    font-size: 23px;
+    margin: 0 0 16px;
+    font-size: 2em;
     color: $gl-gray-dark;
+    padding: 0 0 0.3em;
+    border-bottom: 1px solid $white-dark;
   }
 
   .description {
@@ -34,11 +37,4 @@
       }
     }
   }
-
-  .wiki {
-    code {
-      white-space: pre-wrap;
-      word-break: keep-all;
-    }
-  }
 }
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 21cee2e3a70cced31cea9364f02e9ac5278b8f24..99fdea15218c5278d8ec6e912b2d831b47375b5c 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -33,13 +33,25 @@
       font-size: smaller;
     }
   }
+
+  .file-title {
+    cursor: pointer;
+
+    &:hover {
+      background-color: $dark-background-color;
+    }
+
+    .diff-toggle-caret {
+      padding-right: 6px;
+    }
+  }
+
   .diff-content {
     overflow: auto;
     overflow-y: hidden;
     background: #fff;
     color: #333;
     border-radius: 0 0 3px 3px;
-    -webkit-overflow-scrolling: auto;
 
     .unfold {
       cursor: pointer;
@@ -68,6 +80,11 @@
       border-collapse: separate;
       margin: 0;
       padding: 0;
+      table-layout: fixed;
+
+      .diff-line-num {
+        width: 50px;
+      }
 
       .line_holder td {
         line-height: $code_line_height;
@@ -75,20 +92,6 @@
 
         &.noteable_line {
           position: relative;
-
-          &.old {
-            &:before {
-              content: '-';
-              position: absolute;
-            }
-          }
-
-          &.new {
-            &:before {
-              content: '+';
-              position: absolute;
-            }
-          }
         }
 
         span {
@@ -98,10 +101,6 @@
     }
 
     tr.line_holder.parallel {
-      .old_line, .new_line {
-        min-width: 50px;
-      }
-
       td.line_content.parallel {
         width: 46%;
       }
@@ -111,7 +110,8 @@
       }
     }
 
-    .old_line, .new_line {
+    .old_line,
+    .new_line {
       margin: 0;
       padding: 0;
       border: none;
@@ -122,20 +122,24 @@
       max-width: 50px;
       width: 35px;
       @include user-select(none);
+
       a {
         float: left;
         width: 35px;
         font-weight: normal;
+
         &:hover {
           text-decoration: underline;
         }
       }
     }
+
     .line_content {
       display: block;
       margin: 0;
-      padding: 0 0.5em;
+      padding: 0 1.5em;
       border: none;
+      position: relative;
 
       &.parallel {
         display: table-cell;
@@ -144,16 +148,34 @@
           word-break: break-all;
         }
       }
+
+      &.old {
+        &::before {
+          content: '-';
+          position: absolute;
+          left: 0.5em;
+        }
+      }
+
+      &.new {
+        &::before {
+          content: '+';
+          position: absolute;
+          left: 0.5em;
+        }
+      }
     }
 
     .text-file.diff-wrap-lines table .line_holder td span {
       white-space: pre-wrap;
     }
   }
+
   .image {
     background: #ddd;
     text-align: center;
     padding: 30px;
+
     .wrap {
       display: inline-block;
     }
@@ -162,6 +184,7 @@
       display: inline-block;
       background-color: #fff;
       line-height: 0;
+
       img {
         border: 1px solid #fff;
         background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%),
@@ -170,6 +193,7 @@
         background-position: 0 0, 5px 5px;
         max-width: 100%;
       }
+
       &.deleted {
         border: 1px solid $deleted;
       }
@@ -178,6 +202,7 @@
         border: 1px solid $added;
       }
     }
+
     .image-info {
       font-size: 12px;
       margin: 5px 0 0;
@@ -192,6 +217,7 @@
         margin: auto;
         position: relative;
       }
+
       .swipe-wrap {
         overflow: hidden;
         border-left: 1px solid #999;
@@ -200,10 +226,12 @@
         top: 13px;
         right: 7px;
       }
+
       .frame {
         top: 0;
         right: 0;
         position: absolute;
+
         &.deleted {
           margin: 0;
           display: block;
@@ -211,6 +239,7 @@
           right: 7px;
         }
       }
+
       .swipe-bar {
         display: block;
         height: 100%;
@@ -218,14 +247,17 @@
         z-index: 100;
         position: absolute;
         cursor: pointer;
+
         &:hover {
           .top-handle {
             background-position: -15px 3px;
           }
+
           .bottom-handle {
             background-position: -15px -11px;
           }
         }
+
         .top-handle {
           display: block;
           height: 14px;
@@ -234,6 +266,7 @@
           top: 0;
           background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
         }
+
         .bottom-handle {
           display: block;
           height: 14px;
@@ -251,12 +284,15 @@
         margin: auto;
         position: relative;
       }
-      .frame.added, .frame.deleted {
+
+      .frame.added,
+      .frame.deleted {
         position: absolute;
         display: block;
         top: 0;
         left: 0;
       }
+
       .controls {
         display: block;
         height: 14px;
@@ -310,12 +346,14 @@
     }
     //.view.onion-skin
   }
+
   .view-modes {
     padding: 10px;
     text-align: center;
     background: #eee;
 
-    ul, li {
+    ul,
+    li {
       list-style: none;
       margin: 0;
       padding: 0;
@@ -327,19 +365,24 @@
       border-left: 1px solid #c1c1c1;
       padding: 0 12px 0 16px;
       cursor: pointer;
+
       &:first-child {
         border-left: none;
       }
+
       &:hover {
         text-decoration: underline;
       }
+
       &.active {
         &:hover {
           text-decoration: none;
         }
+
         cursor: default;
         color: #333;
       }
+
       &.disabled {
         display: none;
       }
@@ -431,7 +474,7 @@
 .file-holder {
   .diff-line-num:not(.js-unfold-bottom) {
     a {
-      &:before {
+      &::before {
         content: attr(data-linenumber);
       }
     }
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1aa4e06d97500edce039aa64f3373100e18758fc..778126bcfb79037e434a0bb8d653920caa5cb95b 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -1,7 +1,7 @@
 .file-editor {
   #editor {
     border: none;
-    @include border-radius(0);
+    border-radius: 0;
     height: 500px;
     margin: 0;
     padding: 0;
@@ -15,6 +15,7 @@
 
   .cancel-btn {
     color: #b94a48;
+
     &:hover {
       color: #b94a48;
     }
@@ -54,11 +55,16 @@
     float: left;
   }
 
+  .file-buttons {
+    font-size: 0;
+  }
+
   .select2 {
     float: right;
   }
 
   .encoding-selector,
+  .soft-wrap-toggle,
   .license-selector,
   .gitignore-selector,
   .gitlab-ci-yml-selector {
@@ -67,7 +73,31 @@
     font-family: $regular_font;
   }
 
-  .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
+  .soft-wrap-toggle {
+    margin: 0 $btn-side-margin;
+
+    .soft-wrap {
+      display: block;
+    }
+
+    .no-wrap {
+      display: none;
+    }
+
+    &.soft-wrap-active {
+      .soft-wrap {
+        display: none;
+      }
+
+      .no-wrap {
+        display: block;
+      }
+    }
+  }
+
+  .gitignore-selector,
+  .license-selector,
+  .gitlab-ci-yml-selector {
     .dropdown {
       line-height: 21px;
     }
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 55f9d4a001123f9549b31aace1063f1e85e2bdf3..fc49ff780fc591f4717dffa9a50944d9b10e8085 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,13 +1,26 @@
+.environments-container,
+.deployments-container {
+  width: 100%;
+  overflow: auto;
+}
+
 .environments {
+  .deployment-column {
+    .avatar {
+      float: none;
+    }
+  }
 
   .commit-title {
     margin: 0;
   }
 
-  .fa-play {
-    font-size: 14px;
+  .icon-play {
+    height: 13px;
+    width: 12px;
   }
 
+  .external-url,
   .dropdown-new {
     color: $table-text-gray;
   }
@@ -20,16 +33,43 @@
     }
   }
 
+  .build-link,
   .branch-name {
     color: $gl-dark-link-color;
   }
+
+  .stop-env-link {
+    color: $table-text-gray;
+
+    .stop-env-icon {
+      font-size: 14px;
+    }
+  }
+
+  .deployment {
+    .build-column {
+
+      .build-link {
+        color: $gl-dark-link-color;
+      }
+
+      .avatar {
+        float: none;
+      }
+    }
+  }
 }
 
-.table.builds.environments {
-  min-width: 500px;
+.table.ci-table.environments {
 
   .icon-container {
     width: 20px;
     text-align: center;
   }
+
+  .branch-commit {
+    .commit-id {
+      margin-right: 0;
+    }
+  }
 }
diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss
index 32d2d7b1dbf0399882366f0fa4642e224f6994d3..11309817d31cc88a913da5c2749385618f46e2cf 100644
--- a/app/assets/stylesheets/pages/errors.scss
+++ b/app/assets/stylesheets/pages/errors.scss
@@ -2,7 +2,9 @@
   max-width: 400px;
   margin: 0 auto;
 
-  h1, h2, h3 {
+  h1,
+  h2,
+  h3 {
     text-align: center;
   }
 
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 5c336bb1c7e1b6403f0bee866d94365006b8d08a..3004959ff7bcdc69979ce02e12263e35825e793c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -60,7 +60,7 @@
 
       pre {
         border: none;
-        background: #f9f9f9;
+        background: $gray-light;
         border-radius: 0;
         color: #777;
         margin: 0 20px;
@@ -78,6 +78,7 @@
         margin-bottom: 0;
       }
     }
+
     .event-note-icon {
       color: #777;
       float: left;
@@ -86,21 +87,23 @@
       margin-right: 5px;
     }
   }
+
   .event_icon {
     position: relative;
     float: right;
     border: 1px solid #eee;
     padding: 5px;
-    @include border-radius(5px);
-    background: #f9f9f9;
+    border-radius: 5px;
+    background: $gray-light;
     margin-left: 10px;
     top: -6px;
+
     img {
       width: 20px;
     }
   }
 
-  &:last-child { border: none }
+  &:last-child { border: none; }
 
   .event_commits {
     li {
@@ -109,16 +112,15 @@
         padding: 3px;
         padding-left: 0;
         border: none;
+
         .commit-row-title {
           font-size: $gl-font-size;
         }
       }
 
       &.commits-stat {
-        margin-top: 3px;
         display: block;
-        padding: 3px;
-        padding-left: 0;
+        padding: 0 3px 0 0;
 
         &:hover {
           background: none;
@@ -140,7 +142,7 @@
 .event-last-push {
   overflow: auto;
   width: 100%;
-  
+
   .event-last-push-text {
     @include str-truncated(100%);
     padding: 4px 0;
@@ -161,6 +163,7 @@
       overflow: visible;
       max-width: 100%;
     }
+
     .avatar {
       display: none;
     }
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index b657ca47d38637753b44b19f31b97eea54a3c1cf..4375e29c8db5424f9443086b8fd7eadc17cc01da 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,28 +1,16 @@
-.member-search-form {
-  float: left;
-
-  input[type='search'] {
-    width: 225px;
-    vertical-align: bottom;
-
-    @media (max-width: $screen-xs-max) {
-      width: 100px;
-      vertical-align: bottom;
-    }
-  }
-}
-
 .milestone-row {
   @include str-truncated(90%);
 }
 
 .dashboard .side .panel .panel-heading .input-group {
+
   .form-control {
     height: 42px;
   }
 }
 
 .group-row {
+
   .stats {
     float: right;
     line-height: $list-text-height;
@@ -35,12 +23,14 @@
 }
 
 .ldap-group-links {
+
   .form-actions {
     margin-bottom: $gl-padding;
   }
 }
 
 .groups-cover-block {
+
   .container-fluid {
     position: relative;
   }
@@ -48,10 +38,63 @@
   .group-right-buttons {
     position: absolute;
     right: 16px;
+
     .btn {
       @include btn-gray;
       padding: 3px 10px;
       background-color: $background-color;
     }
   }
+
+  .group-avatar {
+    border: 0;
+  }
+}
+
+.groups-header {
+
+  @media (min-width: $screen-sm-min) {
+    .nav-links {
+      width: 35%;
+    }
+
+    .nav-controls {
+      width: 65%;
+    }
+  }
+}
+
+.groups-empty-state {
+  padding: 50px 100px;
+  overflow: hidden;
+
+  @media (max-width: $screen-md-min) {
+    padding: 50px 0;
+  }
+
+  svg {
+    float: right;
+
+    @media (max-width: $screen-md-min) {
+      float: none;
+      display: block;
+      width: 250px;
+      position: relative;
+      left: 50%;
+      margin-left: -125px;
+    }
+  }
+
+  .text-content {
+    float: left;
+    width: 460px;
+    margin-top: 120px;
+
+    @media (max-width: $screen-md-min) {
+      float: none;
+      margin-top: 60px;
+      width: auto;
+      text-align: center;
+    }
+  }
 }
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 00ab42bec5cb75c36c0efcf35029c8d6f0bc5d1b..a48b4c65db8ea6af15b13e28a2f2ed1c224ca6b2 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -23,28 +23,28 @@
   color: #555;
 
   tbody:first-child tr:first-child {
-    padding-top: 0
+    padding-top: 0;
   }
 
   th {
     padding-top: 15px;
     line-height: 1.5;
     color: #333;
-    text-align: left
+    text-align: left;
   }
 
   td {
     padding-top: 3px;
     padding-bottom: 3px;
     vertical-align: top;
-    line-height: 20px
+    line-height: 20px;
   }
 
   .shortcut {
     padding-right: 10px;
     color: #999;
     text-align: right;
-    white-space: nowrap
+    white-space: nowrap;
   }
 
   .key {
diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..407c8db211d64865e59e8c6fbf8d8c4451f393de
--- /dev/null
+++ b/app/assets/stylesheets/pages/icons.scss
@@ -0,0 +1,12 @@
+// CI icon colors
+
+.ci-status-icon {
+  &-created {
+    fill: $gray-darkest;
+  }
+
+  &-skipped,
+  &-canceled {
+    fill: $gl-text-color;
+  }
+}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 84cc35239f94ceed3198faa50359d8b0282fa028..a4f76a9495a4ad4ad803fee9d001e8d3b1daa655 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,22 +1,3 @@
-i.icon-gitorious {
-  display: inline-block;
-  background-position: 0 0;
-  background-size: contain;
-  background-repeat: no-repeat;
-}
-
-i.icon-gitorious-small {
-  background-image: image-url('gitorious-logo-blue.png');
-  width: 13px;
-  height: 13px;
-}
-
-i.icon-gitorious-big {
-  background-image: image-url('gitorious-logo-black.png');
-  width: 18px;
-  height: 18px;
-}
-
 .import-jobs-from-col,
 .import-jobs-to-col {
   width: 40%;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 46c4a11aa2eb07ad642e3d68bdedd413bfe170e0..230b927a17daaefd7d5d9786b8dbd40c108a5949 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -27,6 +27,7 @@
     margin-right: 5px;
     margin-bottom: 5px;
     display: inline-block;
+
     .color-label {
       padding: 6px 10px;
     }
@@ -128,7 +129,7 @@
   }
 
   .selectbox {
-    display: none
+    display: none;
   }
 
   .btn-clipboard {
@@ -199,14 +200,14 @@
     display: none;
     /* Small devices (tablets, 768px and up) */
     @media (min-width: $screen-sm-min) {
-      display: block
+      display: block;
     }
 
     width: $sidebar_collapsed_width;
     padding-top: 0;
 
     .block {
-      width: $sidebar_collapsed_width - 1px;
+      width: $sidebar_collapsed_width - 2px;
       margin-left: -19px;
       padding: 15px 0 0;
       border-bottom: none;
@@ -276,7 +277,7 @@
     }
 
     &.btn-primary {
-      @extend .btn-primary
+      @extend .btn-primary;
     }
   }
 
@@ -400,7 +401,23 @@
   .js-issuable-selector {
     width: 100%;
   }
+
   @media (max-width: $screen-sm-max) {
     margin-bottom: $gl-padding;
   }
 }
+
+.issuable-list {
+  li {
+    .issue-check {
+      float: left;
+      padding-right: $gl-padding;
+      margin-bottom: 10px;
+      min-width: 15px;
+
+      .selected_issue {
+        vertical-align: text-top;
+      }
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index dfe1e3075dadd45b2404d2db6f0bdbee4d006445..3e7fc3fa52cadfc29f2ae2edc6b3dea51637a22e 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -7,20 +7,9 @@
       margin-bottom: 2px;
     }
 
-    .issue-check {
-      float: left;
-      padding-right: 8px;
-      margin-bottom: 10px;
-      min-width: 15px;
-    }
-
     .issue-labels {
       display: inline-block;
     }
-
-    .issue-no-comments {
-      opacity: 0.5;
-    }
   }
 }
 
@@ -44,7 +33,18 @@ form.edit-issue {
   margin: 0;
 }
 
-.merge-requests-title, .related-branches-title {
+ul.related-merge-requests > li {
+  display: -ms-flexbox;
+  display: -webkit-flex;
+  display: flex;
+
+  .merge-request-id {
+    flex-shrink: 0;
+  }
+}
+
+.merge-requests-title,
+.related-branches-title {
   font-size: 16px;
   font-weight: 600;
 }
@@ -68,12 +68,12 @@ form.edit-issue {
   }
 
   &.closed {
-    background: #f9f9f9;
+    background: $gray-light;
     border-color: #e5e5e5;
   }
 
   &.merged {
-    background: #f9f9f9;
+    background: $gray-light;
     border-color: #e5e5e5;
   }
 }
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 606459f82cd03c89102b723c5db1bacd9081ba49..397f89f501a564c9b9604f9b3a042fa3bc1e9b38 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -1,12 +1,14 @@
 .suggest-colors {
   margin-top: 5px;
+
   a {
-    @include border-radius(4px);
+    border-radius: 4px;
     width: 30px;
     height: 30px;
     display: inline-block;
     margin-right: 10px;
     margin-bottom: 10px;
+    text-decoration: none;
   }
 
   &.suggest-colors-dropdown {
@@ -16,7 +18,7 @@
     overflow: hidden;
 
     a {
-      @include border-radius(0);
+      border-radius: 0;
       width: (100% / 7);
       margin-right: 0;
       margin-bottom: -5px;
@@ -58,6 +60,27 @@
       width: 200px;
       margin-bottom: 0;
     }
+
+    .label {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      vertical-align: middle;
+      max-width: 100%;
+    }
+  }
+
+  .label-type {
+    display: block;
+    margin-bottom: 10px;
+    margin-left: 50px;
+
+    @media (min-width: $screen-sm-min) {
+      display: inline-block;
+      width: 100px;
+      margin-left: 10px;
+      margin-bottom: 0;
+      vertical-align: middle;
+    }
   }
 
   .label-description {
@@ -200,6 +223,13 @@
 }
 
 .label-subscribe-button {
+  .label-subscribe-button-icon {
+    &[disabled] {
+      opacity: 0.5;
+      pointer-events: none;
+    }
+  }
+
   .label-subscribe-button-loading {
     display: none;
   }
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 6926448519e32663effa1342d0e9ecf77b1a0017..8290519dc258074907a97e61638d4a38a07877f7 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -3,6 +3,7 @@
     font-size: 19px;
     color: red;
   }
+
   .correct-syntax {
     font-size: 19px;
     color: #47a447;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 403171d4532e77e518600925e03da9e4dd9d3d10..10f67b4799809ee06d24b9066ef0234469bd0673 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -17,6 +17,7 @@
     line-height: 1.5;
 
     p {
+      font-size: 18px;
       color: #888;
     }
 
@@ -36,10 +37,15 @@
     }
   }
 
-  .login-box {
-    background: #fafafa;
-    border-radius: 10px;
-    box-shadow: 0 0 2px #ccc;
+  p {
+    font-size: 13px;
+  }
+
+  .login-box,
+  .omniauth-container {
+    box-shadow: 0 0 0 1px $border-color;
+    border-bottom-right-radius: 2px;
+    border-bottom-left-radius: 2px;
     padding: 15px;
 
     .login-heading h3 {
@@ -58,42 +64,133 @@
 
     a.forgot {
       float: right;
-      padding-top: 6px
+      padding-top: 6px;
     }
 
     .nav .active a {
       background: transparent;
     }
+
+    // Styles the glowing border of focused input for username async validation
+    .login-body {
+      font-size: 13px;
+
+      input + p {
+        margin-top: 5px;
+      }
+
+      .username .validation-success {
+        color: $green-normal;
+      }
+
+      .username .validation-error {
+        color: $red-normal;
+      }
+    }
   }
 
-  .form-control {
-    font-size: 14px;
-    padding: 10px 8px;
-    width: 100%;
-    height: auto;
+  .omniauth-container {
+    p {
+      margin: 0;
+    }
+  }
+
+  .new-session-tabs {
+    display: -webkit-flex;
+    display: flex;
+    box-shadow: 0 0 0 1px $border-color;
+    border-top-right-radius: $border-radius-default;
+    border-top-left-radius: $border-radius-default;
+
+    li {
+      flex: 1;
+      text-align: center;
+
+      &:first-of-type {
+        border-top-left-radius: $border-radius-default;
+      }
+
+      &:last-of-type {
+        border-left: 1px solid $border-color;
+        border-top-right-radius: $border-radius-default;
+      }
+
+      &:not(.active) {
+        background-color: $gray-light;
+        border-left: 1px solid $border-color;
+      }
+
+      a {
+        width: 100%;
+        font-size: 18px;
+
+        &:hover {
+          border: 1px solid transparent;
+        }
+      }
+
+      &.active {
+        border-bottom: 1px solid $border-color;
 
-    &.top {
-      @include border-radius(5px 5px 0 0);
-      margin-bottom: 0;
+        a {
+          border: none;
+          border-bottom: 2px solid $link-underline-blue;
+          color: $black;
+
+          &:hover {
+            border-bottom: 2px solid $link-underline-blue;
+          }
+        }
+      }
     }
+  }
+
+  // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+  // These styles prevent this from breaking the layout, and only applied when providers are configured.
+
+  .new-session-tabs.custom-provider-tabs {
+    flex-wrap: wrap;
 
-    &.bottom {
-      @include border-radius(0 0 5px 5px);
-      border-top: 0;
-      margin-bottom: 20px;
+    li {
+      min-width: 85px;
+      flex-basis: auto;
+
+      // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+      // We are making somewhat of an assumption about the configuration here: that users do not have more than
+      // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+      // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+      // above one of the bottom row elements. If you know a better way, please implement it!
+      &:nth-child(n+5) {
+        border-top: 1px solid $border-color;
+      }
     }
 
-    &.middle {
-      border-top: 0;
-      margin-bottom: 0;
-      @include border-radius(0);
+    a {
+      font-size: 16px;
     }
+  }
 
-    &:active, &:focus {
+
+  .form-control {
+    &:active,
+    &:focus {
       background-color: #fff;
     }
   }
 
+  label {
+    font-weight: normal;
+  }
+
+  .submit-container {
+    margin-top: 16px;
+  }
+
+  input[type="submit"] {
+    @extend .btn-block;
+    margin-bottom: 0;
+  }
+
   .devise-errors {
     h2 {
       margin-top: 0;
@@ -101,20 +198,13 @@
       color: #a00;
     }
   }
-
-  .remember-me {
-    margin-top: -10px;
-
-    label {
-      font-weight: normal;
-    }
-  }
 }
 
 @media (max-width: $screen-xs-max) {
   .login-page {
     .col-sm-5.pull-right {
       float: none !important;
+      margin-bottom: 45px;
     }
   }
 }
@@ -127,3 +217,64 @@
     height: 32px;
   }
 }
+
+.devise-layout-html {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+}
+
+// Fixes footer container to bottom of viewport
+.devise-layout-html body {
+  // offset height of fixed header + 1 to avoid scroll
+  height: calc(100% - 51px);
+  margin: 0;
+  padding: 0;
+
+  .page-wrap {
+    min-height: 100%;
+    position: relative;
+  }
+
+  .footer-container,
+  hr.footer-fixed {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 40px;
+    background: $white-light;
+  }
+
+  .navless-container {
+    padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+
+    @media (max-width: $screen-xs-max) {
+      padding: 0 15px 65px;
+    }
+  }
+}
+
+// For sign in pane only, to improve tab order, the following removes the submit button from
+// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
+
+.login-box {
+  .new_user {
+    position: relative;
+    padding-bottom: 35px;
+
+    @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+      .forgot-password {
+        float: none !important;
+        margin-top: 5px;
+      }
+    }
+  }
+
+  .move-submit-down {
+    position: absolute;
+    width: 100%;
+    bottom: 0;
+  }
+}
+
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
new file mode 100644
index 0000000000000000000000000000000000000000..756efa9c7fa027433b0643a602b32f02e10c0166
--- /dev/null
+++ b/app/assets/stylesheets/pages/members.scss
@@ -0,0 +1,98 @@
+.project-members-title {
+  padding-bottom: 10px;
+  border-bottom: 1px solid $border-color;
+}
+
+.member {
+  .list-item-name {
+    @media (min-width: $screen-sm-min) {
+      float: left;
+      width: 50%;
+    }
+
+    strong {
+      font-weight: 600;
+    }
+  }
+
+  .controls {
+    @media (min-width: $screen-sm-min) {
+      display: -webkit-flex;
+      display: flex;
+      width: 400px;
+      max-width: 50%;
+    }
+  }
+
+  .form-horizontal {
+    margin-top: 5px;
+
+    @media (min-width: $screen-sm-min) {
+      display: -webkit-flex;
+      display: flex;
+      width: 100%;
+      margin-top: 3px;
+    }
+  }
+
+  .btn-remove {
+    width: 100%;
+
+    @media (min-width: $screen-sm-min) {
+      width: auto;
+    }
+  }
+}
+
+.member-form-control {
+  @media (max-width: $screen-xs-max) {
+    padding: 5px 0;
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  @media (min-width: $screen-sm-min) {
+    width: 50%;
+  }
+}
+
+.member-access-text {
+  margin-left: auto;
+  line-height: 43px;
+}
+
+.member.existing-title {
+  @media (min-width: $screen-sm-min) {
+    float: left;
+  }
+}
+
+.member-search-form {
+  position: relative;
+
+  @media (min-width: $screen-sm-min) {
+    float: right;
+  }
+
+  .form-control {
+    width: 100%;
+    padding-right: 35px;
+
+    @media (min-width: $screen-sm-min) {
+      width: 350px;
+    }
+  }
+}
+
+.member-search-btn {
+  position: absolute;
+  right: 0;
+  top: 0;
+  height: 35px;
+  padding-left: 10px;
+  padding-right: 10px;
+  color: $gray-darkest;
+  background: transparent;
+  border: 0;
+  outline: 0;
+}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..19ab198c2e731bb051fbb977e7e0942e650b5201
--- /dev/null
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -0,0 +1,287 @@
+$colors: (
+  white_header_head_neutral   : #e1fad7,
+  white_line_head_neutral     : #effdec,
+  white_button_head_neutral   : #9adb84,
+
+  white_header_head_chosen    : #baf0a8,
+  white_line_head_chosen      : #e1fad7,
+  white_button_head_chosen    : #52c22d,
+
+  white_header_origin_neutral : #e0f0ff,
+  white_line_origin_neutral   : #f2f9ff,
+  white_button_origin_neutral : #87c2fa,
+
+  white_header_origin_chosen  : #add8ff,
+  white_line_origin_chosen    : #e0f0ff,
+  white_button_origin_chosen  : #268ced,
+
+  white_header_not_chosen     : #f0f0f0,
+  white_line_not_chosen       : $gray-light,
+
+
+  dark_header_head_neutral   : rgba(#3f3, .2),
+  dark_line_head_neutral     : rgba(#3f3, .1),
+  dark_button_head_neutral   : #40874f,
+
+  dark_header_head_chosen    : rgba(#3f3, .33),
+  dark_line_head_chosen      : rgba(#3f3, .2),
+  dark_button_head_chosen    : #258537,
+
+  dark_header_origin_neutral : rgba(#2878c9, .4),
+  dark_line_origin_neutral   : rgba(#2878c9, .3),
+  dark_button_origin_neutral : #2a5c8c,
+
+  dark_header_origin_chosen  : rgba(#2878c9, .6),
+  dark_line_origin_chosen    : rgba(#2878c9, .4),
+  dark_button_origin_chosen  : #1d6cbf,
+
+  dark_header_not_chosen     : rgba(#fff, .25),
+  dark_line_not_chosen       : rgba(#fff, .1),
+
+
+  monokai_header_head_neutral   : rgba(#a6e22e, .25),
+  monokai_line_head_neutral     : rgba(#a6e22e, .1),
+  monokai_button_head_neutral   : #376b20,
+
+  monokai_header_head_chosen    : rgba(#a6e22e, .4),
+  monokai_line_head_chosen      : rgba(#a6e22e, .25),
+  monokai_button_head_chosen    : #39800d,
+
+  monokai_header_origin_neutral : rgba(#60d9f1, .35),
+  monokai_line_origin_neutral   : rgba(#60d9f1, .15),
+  monokai_button_origin_neutral : #38848c,
+
+  monokai_header_origin_chosen  : rgba(#60d9f1, .5),
+  monokai_line_origin_chosen    : rgba(#60d9f1, .35),
+  monokai_button_origin_chosen  : #3ea4b2,
+
+  monokai_header_not_chosen     : rgba(#76715d, .24),
+  monokai_line_not_chosen       : rgba(#76715d, .1),
+
+
+  solarized_light_header_head_neutral   : rgba(#859900, .37),
+  solarized_light_line_head_neutral     : rgba(#859900, .2),
+  solarized_light_button_head_neutral   : #afb262,
+
+  solarized_light_header_head_chosen    : rgba(#859900, .5),
+  solarized_light_line_head_chosen      : rgba(#859900, .37),
+  solarized_light_button_head_chosen    : #94993d,
+
+  solarized_light_header_origin_neutral : rgba(#2878c9, .37),
+  solarized_light_line_origin_neutral   : rgba(#2878c9, .15),
+  solarized_light_button_origin_neutral : #60a1bf,
+
+  solarized_light_header_origin_chosen  : rgba(#2878c9, .6),
+  solarized_light_line_origin_chosen    : rgba(#2878c9, .37),
+  solarized_light_button_origin_chosen  : #2482b2,
+
+  solarized_light_header_not_chosen     : rgba(#839496, .37),
+  solarized_light_line_not_chosen       : rgba(#839496, .2),
+
+
+  solarized_dark_header_head_neutral   : rgba(#859900, .35),
+  solarized_dark_line_head_neutral     : rgba(#859900, .15),
+  solarized_dark_button_head_neutral   : #376b20,
+
+  solarized_dark_header_head_chosen    : rgba(#859900, .5),
+  solarized_dark_line_head_chosen      : rgba(#859900, .35),
+  solarized_dark_button_head_chosen    : #39800d,
+
+  solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
+  solarized_dark_line_origin_neutral   : rgba(#2878c9, .15),
+  solarized_dark_button_origin_neutral : #086799,
+
+  solarized_dark_header_origin_chosen  : rgba(#2878c9, .6),
+  solarized_dark_line_origin_chosen    : rgba(#2878c9, .35),
+  solarized_dark_button_origin_chosen  : #0082cc,
+
+  solarized_dark_header_not_chosen     : rgba(#839496, .25),
+  solarized_dark_line_not_chosen       : rgba(#839496, .15)
+);
+
+
+@mixin color-scheme($color) {
+  .header.line_content,
+  .diff-line-num {
+    &.origin {
+      background-color: map-get($colors, #{$color}_header_origin_neutral);
+      border-color: map-get($colors, #{$color}_header_origin_neutral);
+
+      button {
+        background-color: map-get($colors, #{$color}_button_origin_neutral);
+        border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
+      }
+
+      &.selected {
+        background-color: map-get($colors, #{$color}_header_origin_chosen);
+        border-color: map-get($colors, #{$color}_header_origin_chosen);
+
+        button {
+          background-color: map-get($colors, #{$color}_button_origin_chosen);
+          border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
+        }
+      }
+
+      &.unselected {
+        background-color: map-get($colors, #{$color}_header_not_chosen);
+        border-color: map-get($colors, #{$color}_header_not_chosen);
+
+        button {
+          background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
+          border-color: map-get($colors, #{$color}_button_origin_neutral);
+        }
+      }
+    }
+
+    &.head {
+      background-color: map-get($colors, #{$color}_header_head_neutral);
+      border-color: map-get($colors, #{$color}_header_head_neutral);
+
+      button {
+        background-color: map-get($colors, #{$color}_button_head_neutral);
+        border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
+      }
+
+      &.selected {
+        background-color: map-get($colors, #{$color}_header_head_chosen);
+        border-color: map-get($colors, #{$color}_header_head_chosen);
+
+        button {
+          background-color: map-get($colors, #{$color}_button_head_chosen);
+          border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
+        }
+      }
+
+      &.unselected {
+        background-color: map-get($colors, #{$color}_header_not_chosen);
+        border-color: map-get($colors, #{$color}_header_not_chosen);
+
+        button {
+          background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
+          border-color: map-get($colors, #{$color}_button_head_neutral);
+        }
+      }
+    }
+  }
+
+  .line_content {
+    &.origin {
+      background-color: map-get($colors, #{$color}_line_origin_neutral);
+
+      &.selected {
+        background-color: map-get($colors, #{$color}_line_origin_chosen);
+      }
+
+      &.unselected {
+        background-color: map-get($colors, #{$color}_line_not_chosen);
+      }
+    }
+
+    &.head {
+      background-color: map-get($colors, #{$color}_line_head_neutral);
+
+      &.selected {
+        background-color: map-get($colors, #{$color}_line_head_chosen);
+      }
+
+      &.unselected {
+        background-color: map-get($colors, #{$color}_line_not_chosen);
+      }
+    }
+  }
+}
+
+
+#conflicts {
+
+  .white {
+    @include color-scheme('white')
+  }
+
+  .dark {
+    @include color-scheme('dark')
+  }
+
+  .monokai {
+    @include color-scheme('monokai')
+  }
+
+  .solarized-light {
+    @include color-scheme('solarized_light')
+  }
+
+  .solarized-dark {
+    @include color-scheme('solarized_dark')
+  }
+
+  .diff-wrap-lines .line_content {
+    white-space: normal;
+    min-height: 19px;
+  }
+
+  .line_content.header {
+    position: relative;
+
+    button {
+      border-radius: 2px;
+      font-size: 10px;
+      position: absolute;
+      right: 10px;
+      padding: 0;
+      color: #fff;
+      width: 75px; // static width to make 2 buttons have same width
+      height: 19px;
+    }
+  }
+
+  .btn-success .fa-spinner {
+    color: #fff;
+  }
+
+  .editor-wrap {
+    &.is-loading {
+      .editor {
+        display: none;
+      }
+
+      .loading {
+        display: block;
+      }
+    }
+
+    &.saved {
+      .editor {
+        border-top: solid 2px $border-green-extra-light;
+      }
+    }
+
+    .editor {
+      pre {
+        height: 350px;
+        border: none;
+        border-radius: 0;
+        margin-bottom: 0;
+      }
+    }
+
+    .loading {
+      display: none;
+    }
+  }
+
+  .discard-changes-alert {
+    background-color: $background-color;
+    text-align: right;
+    padding: $gl-padding-top $gl-padding;
+    color: $gl-text-color;
+
+    .discard-actions {
+      display: inline-block;
+      margin-left: 10px;
+    }
+  }
+
+  .resolve-conflicts-form {
+    padding-top: $gl-padding;
+  }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b463626951849c30664185ed8630981257c577d4..f8e31a624ecdf2b448acae39f5e542ac8ac6282e 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -6,10 +6,11 @@
   background: $background-color;
   color: $gl-gray;
   border: 1px solid $border-color;
-  @include border-radius(2px);
+  border-radius: 2px;
 
   form {
     margin-bottom: 0;
+
     .clearfix {
       margin-bottom: 0;
     }
@@ -46,6 +47,7 @@
 
       &.right {
         float: right;
+
         a {
           color: $gl-gray;
         }
@@ -58,7 +60,7 @@
   }
 
   .ci_widget {
-    border-bottom: 1px solid #eef0f2;
+    border-bottom: 1px solid $widget-inner-border;
 
     svg {
       margin-right: 4px;
@@ -70,7 +72,8 @@
     &.ci-success {
       color: $gl-success;
 
-      a.environment {
+      a.environment,
+      a.pipeline {
         color: inherit;
       }
     }
@@ -120,6 +123,10 @@
     color: #5c5d5e;
   }
 
+  .js-deployment-link {
+    display: inline-block;
+  }
+
   .mr-widget-body {
     h4 {
       font-weight: 600;
@@ -176,6 +183,15 @@
   .ci-coverage {
     float: right;
   }
+
+  .stop-env-container {
+    color: $gl-text-color;
+    float: right;
+
+    a {
+      color: $gl-text-color;
+    }
+  }
 }
 
 .mr_source_commit,
@@ -187,6 +203,7 @@
     padding-top: 2px;
     padding-bottom: 2px;
     list-style: none;
+
     &:hover {
       background: none;
     }
@@ -203,6 +220,19 @@
   word-break: break-all;
 }
 
+.commits-empty {
+  text-align: center;
+
+  h4 {
+    padding-top: 20px;
+    padding-bottom: 10px;
+  }
+
+  svg {
+    width: 230px;
+  }
+}
+
 .mr-list {
   .merge-request {
     padding: 10px 15px;
@@ -231,10 +261,6 @@
   .merge-request-labels {
     display: inline-block;
   }
-
-  .merge-request-no-comments {
-    opacity: 0.5;
-  }
 }
 
 .merge-request-angle {
@@ -253,6 +279,10 @@
 #modal_merge_info .modal-dialog {
   width: 600px;
 
+  .dark {
+    margin-right: 40px;
+  }
+
   .btn-clipboard {
     @extend .pull-right;
 
@@ -267,12 +297,6 @@
   line-height: 31px;
 }
 
-.builds {
-  .table-holder {
-    overflow-x: scroll;
-  }
-}
-
 .panel-new-merge-request {
   .panel-heading {
     padding: 5px 10px;
@@ -353,10 +377,14 @@
 .issuable-form-select-holder {
   display: inline-block;
   width: 250px;
+
+  .dropdown-menu-toggle {
+    width: 100%;
+  }
 }
 
 .table-holder {
-  .builds {
+  .ci-table {
 
     th {
       background-color: $white-light;
@@ -374,3 +402,58 @@
     }
   }
 }
+
+.mr-version-controls {
+  background: $background-color;
+  border-bottom: 1px solid $border-color;
+  color: $gl-text-color;
+
+  .mr-version-menus-container {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    padding: 16px;
+  }
+
+  .content-block {
+    border-top: 1px solid $border-color;
+    padding: $gl-padding-top $gl-padding;
+  }
+
+  .comments-disabled-notif {
+    .btn {
+      margin-left: 5px;
+    }
+  }
+
+  .mr-version-dropdown,
+  .mr-version-compare-dropdown {
+    margin: 0 7px;
+  }
+
+  .dropdown-title {
+    color: $gl-text-color;
+  }
+
+  .fa-info-circle {
+    color: $orange-normal;
+    padding-right: 5px;
+  }
+}
+
+.merge-request-tabs-holder {
+  background-color: $white-light;
+
+  &.affix {
+    top: 100px;
+    left: 0;
+    z-index: 10;
+    transition: right .15s;
+  }
+
+  &:not(.affix) .container-fluid {
+    padding-left: 0;
+    padding-right: 0;
+  }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b94f524b51339b1f243358c2755788242c9c19e3..13402acd8e17fecbac31228f4cff704ef9118bad 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -2,13 +2,17 @@
   max-width: 90%;
 }
 
-li.milestone {
-  h4 {
-    font-weight: bold;
-  }
+.milestones {
+  .milestone {
+    padding: 10px 16px;
+
+    h4 {
+      font-weight: bold;
+    }
 
-  .progress {
-    height: 6px;
+    .progress {
+      height: 6px;
+    }
   }
 }
 
@@ -29,6 +33,7 @@ li.milestone {
     // Issue title
     span a {
       color: $gl-text-color;
+      word-wrap: break-word;
     }
   }
 }
@@ -45,7 +50,8 @@ li.milestone {
   }
 }
 
-.issues-sortable-list, .merge_requests-sortable-list {
+.issues-sortable-list,
+.merge_requests-sortable-list {
   .issuable-detail {
     display: block;
     margin-top: 7px;
@@ -54,6 +60,7 @@ li.milestone {
       color: $gl-placeholder-color;
       margin-right: 5px;
     }
+
     .avatar {
       float: none;
     }
@@ -64,3 +71,14 @@ li.milestone {
   border-bottom: 1px solid $border-color;
   padding: 20px 0;
 }
+
+@media (max-width: $screen-sm-min) {
+  .milestone-actions {
+    @include clearfix();
+    padding-top: $gl-vert-padding;
+
+    .btn:first-child {
+      margin-left: 0;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3784010348a0dcdbfa0876081f16283d08c7197d..16ddef481bd362f119fde963a9268bb56ba70321 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -8,9 +8,10 @@
 .diff-file .diff-content {
   tr.line_holder:hover > td .line_note_link {
     opacity: 1.0;
-    filter: alpha(opacity=100);
+    filter: alpha(opacity = 100);
   }
 }
+
 .diff-file,
 .discussion {
   .new-note {
@@ -23,7 +24,8 @@
   display: none;
 }
 
-.new-note, .note-edit-form {
+.new-note,
+.note-edit-form {
   .note-form-actions {
     margin-top: $gl-padding;
   }
@@ -159,6 +161,32 @@
   }
 }
 
+.discussion-with-resolve-btn {
+  display: table;
+  width: 100%;
+  border-collapse: separate;
+  table-layout: auto;
+
+  .btn-group {
+    display: table-cell;
+    float: none;
+    width: 1%;
+
+    &:first-child {
+      width: 100%;
+      padding-right: 5px;
+    }
+
+    &:last-child {
+      padding-left: 5px;
+    }
+  }
+
+  .btn {
+    width: 100%;
+  }
+}
+
 .discussion-notes-count {
   font-size: 16px;
 }
@@ -168,6 +196,7 @@
     min-height: 140px;
     max-height: 500px;
   }
+
   .note-form-actions {
     background: transparent;
   }
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a2b5437e5031080ed00835470d1967fd7d17bf70..526e9ae5cdda3b73731a19b54d7ee8bff616f4d1 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -28,7 +28,8 @@ ul.notes {
     }
   }
 
-  .note-created-ago, .note-updated-at {
+  .note-created-ago,
+  .note-updated-at {
     white-space: nowrap;
   }
 
@@ -104,11 +105,6 @@ ul.notes {
         padding: 2px;
         margin-top: 10px;
       }
-
-      .award-control {
-        font-size: 13px;
-        padding: 2px 5px;
-      }
     }
 
     .note-header {
@@ -147,9 +143,18 @@ ul.notes {
 
 // Diff code in discussion view
 .discussion-body .diff-file {
+  .file-title {
+    cursor: default;
+
+    &:hover {
+      background-color: $gray-light;
+    }
+  }
+
   .diff-header > span {
     margin-right: 10px;
   }
+
   .line_content {
     white-space: pre-wrap;
   }
@@ -281,19 +286,13 @@ ul.notes {
     font-size: 17px;
   }
 
-  &.js-note-delete {
-    i {
-      &:hover {
-        color: $gl-text-red;
-      }
+  &:hover {
+    .danger-highlight {
+      color: $gl-text-red;
     }
-  }
 
-  &.js-note-edit {
-    i {
-      &:hover {
-        color: $gl-link-color;
-      }
+    .link-highlight {
+      color: $gl-link-color;
     }
   }
 }
@@ -340,7 +339,7 @@ ul.notes {
 
   .add-diff-note {
     margin-top: -4px;
-    @include border-radius(40px);
+    border-radius: 40px;
     background: #fff;
     padding: 4px;
     font-size: 16px;
@@ -351,6 +350,7 @@ ul.notes {
     width: 32px;
     // "hide" it by default
     display: none;
+
     &:hover {
       background: $gl-info;
       color: #fff;
@@ -383,3 +383,80 @@ ul.notes {
     color: $gl-link-color;
   }
 }
+
+.line-resolve-all-container {
+  .btn-group {
+    margin-top: -1px;
+    margin-left: -4px;
+  }
+
+  .discussion-next-btn {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+}
+
+.line-resolve-all {
+  display: inline-block;
+  padding: 5px 10px;
+  background-color: $background-color;
+  border: 1px solid $border-color;
+  border-radius: $border-radius-default;
+
+  &.has-next-btn {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .line-resolve-btn {
+    vertical-align: middle;
+    margin-right: 5px;
+  }
+}
+
+.line-resolve-text {
+  vertical-align: middle;
+}
+
+.line-resolve-btn {
+  display: inline-block;
+  position: relative;
+  top: 2px;
+  padding: 0;
+  background-color: transparent;
+  border: none;
+  outline: 0;
+
+  &.is-disabled {
+    cursor: default;
+  }
+
+  &:not(.is-disabled):hover,
+  &:not(.is-disabled):focus,
+  &.is-active {
+    color: $gl-text-green;
+
+    svg path {
+      fill: $gl-text-green;
+    }
+  }
+
+  svg {
+    position: relative;
+    color: $notes-action-color;
+
+    path {
+      fill: $notes-action-color;
+    }
+  }
+}
+
+.discussion-next-btn {
+  svg {
+    margin: 0;
+
+    path {
+      fill: $gray-darkest;
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 21919fe4d73ff3d28d9c65c5dcf5e307cf7cbef3..bf3cb6e7ad9defb03b10e41a5ee6d803ca414153 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -2,6 +2,7 @@
   .stage {
     max-width: 90px;
     width: 90px;
+    text-align: center;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
@@ -19,8 +20,20 @@
     margin: 4px;
   }
 
-  .table.builds {
+  .table.ci-table {
     min-width: 1200px;
+
+    .pipeline-id {
+      color: $black;
+    }
+
+    .branch-commit {
+      width: 30%;
+
+      .branch-name {
+        max-width: 195px;
+      }
+    }
   }
 }
 
@@ -38,13 +51,20 @@
   overflow: auto;
 }
 
-.table.builds {
+.table.ci-table {
   min-width: 900px;
 
   &.pipeline {
     min-width: 650px;
   }
 
+  &.builds-page {
+
+    tr {
+      height: 71px;
+    }
+  }
+
   tr {
     th {
       padding: 16px 8px;
@@ -60,7 +80,16 @@
     border-top-width: 1px;
   }
 
+  .build.retried {
+    background-color: $gray-lightest;
+  }
+
   .commit-link {
+    a {
+      &:focus {
+        text-decoration: none;
+      }
+    }
 
     .ci-status {
 
@@ -75,6 +104,15 @@
     }
   }
 
+  .avatar {
+    margin-left: 0;
+    float: none;
+  }
+
+  .api {
+    color: $code-color;
+  }
+
   .branch-commit {
 
     .branch-name {
@@ -96,12 +134,11 @@
 
     .fa {
       font-size: 12px;
-      color: $table-text-gray;
+      color: $gl-text-color;
     }
 
     .commit-id {
       color: $gl-link-color;
-      margin-right: 8px;
     }
 
     .commit-title {
@@ -112,10 +149,6 @@
       text-overflow: ellipsis;
     }
 
-    .avatar {
-      margin-left: 0;
-    }
-
     .label {
       margin-right: 4px;
     }
@@ -131,31 +164,49 @@
 
   .icon-container {
     display: inline-block;
-    text-align: right;
-    width: 15px;
+    width: 10px;
 
-    .fa {
-      position: relative;
-      right: 3px;
-    }
-
-    svg {
-      position: relative;
-      right: 1px;
+    &.commit-icon {
+      width: 15px;
+      text-align: center;
     }
   }
 
   .stage-cell {
+    font-size: 0;
 
     svg {
       height: 18px;
       width: 18px;
+      position: relative;
+      z-index: 2;
       vertical-align: middle;
       overflow: visible;
     }
 
-    .light {
-      width: 3px;
+    .stage-container {
+      display: inline-block;
+      position: relative;
+      margin-right: 6px;
+
+      .tooltip {
+        white-space: nowrap;
+      }
+
+      &:not(:last-child) {
+        &::after {
+          content: '';
+          width: 8px;
+          position: absolute;
+          right: -7px;
+          bottom: 8px;
+          border-bottom: 2px solid $border-color;
+        }
+      }
+
+      a {
+        display: block;
+      }
     }
   }
 
@@ -199,9 +250,13 @@
 
       .fa {
         color: $table-text-gray;
-        margin-right: 6px;
         font-size: 14px;
       }
+
+      svg,
+      .fa {
+        margin-right: 0;
+      }
     }
 
     .btn-remove {
@@ -215,6 +270,13 @@
           border-color: $border-white-normal;
         }
       }
+
+      .btn {
+        .icon-play {
+          height: 13px;
+          width: 12px;
+        }
+      }
     }
   }
 
@@ -229,3 +291,411 @@
     box-shadow: none;
   }
 }
+
+// Pipeline visualization
+
+.toggle-pipeline-btn {
+  background-color: $gray-dark;
+
+  &.graph-collapsed {
+    background-color: $white-light;
+  }
+}
+
+.pipeline-graph {
+  width: 100%;
+  overflow: auto;
+  white-space: nowrap;
+  transition: max-height 0.3s, padding 0.3s;
+
+  &.graph-collapsed {
+    max-height: 0;
+    padding: 0 16px;
+  }
+}
+
+.pipeline-visualization {
+  position: relative;
+
+  ul {
+    padding: 0;
+  }
+}
+
+.stage-column {
+  display: inline-block;
+  vertical-align: top;
+
+  &:not(:last-child) {
+    margin-right: 44px;
+  }
+
+  &.left-margin {
+    &:not(:first-child) {
+      margin-left: 44px;
+
+      .left-connector {
+        &::before {
+          content: '';
+          position: absolute;
+          top: 48%;
+          left: -48px;
+          border-top: 2px solid $border-color;
+          width: 48px;
+          height: 1px;
+        }
+      }
+    }
+  }
+
+  &.no-margin {
+    margin: 0;
+  }
+
+  li {
+    list-style: none;
+  }
+
+  .stage-name {
+    margin: 0 0 15px 10px;
+    font-weight: bold;
+    width: 176px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .build {
+    border: 1px solid $border-color;
+    position: relative;
+    padding: 7px 10px 8px;
+    border-radius: 30px;
+    width: 186px;
+    margin-bottom: 10px;
+
+    &:hover {
+      background-color: $gray-lighter;
+    }
+
+    &.playable {
+
+      svg {
+        height: 13px;
+        width: 20px;
+        position: relative;
+        top: 1px;
+
+        path {
+          fill: $layout-link-gray;
+        }
+      }
+    }
+
+    .build-content {
+      display: -ms-flexbox;
+      display: -webkit-flex;
+      display: flex;
+      width: 164px;
+
+      .ci-status-icon {
+        svg {
+          height: 20px;
+          width: 20px;
+        }
+      }
+
+      .tooltip {
+        white-space: nowrap;
+
+        .tooltip-inner {
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+      }
+
+      .ci-status-text {
+        width: 135px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        vertical-align: middle;
+        display: inline-block;
+        position: relative;
+        top: -1px;
+      }
+
+      a {
+        color: $gl-text-color-light;
+        text-decoration: none;
+      }
+
+      .dropdown-menu-toggle {
+        background-color: transparent;
+        border: none;
+        width: auto;
+        padding: 0;
+        color: $gl-text-color-light;
+        flex-grow: 1;
+
+        .ci-status-text {
+          max-width: 112px;
+          width: auto;
+        }
+      }
+
+      .grouped-pipeline-dropdown {
+        padding: 0;
+        width: 186px;
+        left: auto;
+        right: -197px;
+        top: -9px;
+
+        ul {
+          max-height: 245px;
+          overflow: auto;
+
+          li:first-child {
+            padding-top: 8px;
+          }
+
+          li:last-child {
+            padding-bottom: 8px;
+          }
+        }
+
+        a {
+          color: $gl-text-color;
+          padding: 7px 8px 8px;
+
+          &:hover {
+            background-color: $blue-light-transparent;
+            border-radius: 3px;
+
+            .ci-status-text {
+              text-decoration: none;
+            }
+          }
+        }
+
+        svg {
+          width: 14px;
+          height: 14px;
+        }
+
+        .ci-status-text {
+          width: 112px;
+        }
+
+        .arrow {
+          &::before,
+          &::after {
+            content: '';
+            display: inline-block;
+            position: absolute;
+            width: 0;
+            height: 0;
+            border-color: transparent;
+            border-style: solid;
+            top: 18px;
+          }
+
+          &::before {
+            left: -5px;
+            margin-top: -6px;
+            border-width: 7px 5px 7px 0;
+            border-right-color: $border-color;
+          }
+
+          &::after {
+            left: -4px;
+            margin-top: -9px;
+            border-width: 10px 7px 10px 0;
+            border-right-color: $white-light;
+          }
+        }
+      }
+
+      .badge {
+        background-color: $gray-darker;
+        color: $gl-text-color-light;
+        font-weight: normal;
+        margin-left: $btn-xs-side-margin;
+      }
+    }
+
+    svg {
+      vertical-align: middle;
+      margin-right: 5px;
+    }
+
+    // Connect first build in each stage with right horizontal line
+    &:first-child {
+      &::after {
+        content: '';
+        position: absolute;
+        top: 48%;
+        right: -48px;
+        border-top: 2px solid $border-color;
+        width: 48px;
+        height: 1px;
+      }
+    }
+
+    // Connect each build (except for first) with curved lines
+    &:not(:first-child) {
+      &::after,
+      &::before {
+        content: '';
+        top: -49px;
+        position: absolute;
+        border-bottom: 2px solid $border-color;
+        width: 25px;
+        height: 69px;
+      }
+
+      // Right connecting curves
+      &::after {
+        right: -25px;
+        border-right: 2px solid $border-color;
+        border-radius: 0 0 20px;
+      }
+
+      // Left connecting curves
+      &::before {
+        left: -25px;
+        border-left: 2px solid $border-color;
+        border-radius: 0 0 0 20px;
+      }
+    }
+
+    // Connect second build to first build with smaller curved line
+    &:nth-child(2) {
+      &::after,
+      &::before {
+        height: 29px;
+        top: -9px;
+      }
+
+      .curve {
+        display: block;
+      }
+    }
+  }
+
+  &:last-child {
+    .build {
+      // Remove right connecting horizontal line from first build in last stage
+      &:first-child {
+        &::after {
+          border: none;
+        }
+      }
+      // Remove right curved connectors from all builds in last stage
+      &:not(:first-child) {
+        &::after {
+          border: none;
+        }
+      }
+      // Remove opposite curve
+      .curve {
+        &::before {
+          display: none;
+        }
+      }
+    }
+  }
+
+  &:first-child {
+    .build {
+      // Remove left curved connectors from all builds in first stage
+      &:not(:first-child) {
+        &::before {
+          border: none;
+        }
+      }
+      // Remove opposite curve
+      .curve {
+        &::after {
+          display: none;
+        }
+      }
+    }
+  }
+
+  // Curve first child connecting lines in opposite direction
+  .curve {
+    display: none;
+
+    &::before,
+    &::after {
+      content: '';
+      width: 21px;
+      height: 25px;
+      position: absolute;
+      top: -32px;
+      border-top: 2px solid $border-color;
+    }
+
+    &::after {
+      left: -44px;
+      border-right: 2px solid $border-color;
+      border-radius: 0 20px;
+    }
+
+    &::before {
+      right: -44px;
+      border-left: 2px solid $border-color;
+      border-radius: 20px 0 0;
+    }
+  }
+}
+
+.pipeline-actions {
+  border-bottom: none;
+}
+
+.toggle-pipeline-btn {
+
+  .fa {
+    color: $dropdown-header-color;
+  }
+}
+
+.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;
+      }
+    }
+  }
+}
+
+.ci-status-icon-created {
+
+  svg {
+    fill: $gray-darkest;
+  }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871fa7569c69b5feed3d6b9c698b1356..6fab97a71aacf411c8e094108c16bca3f361d2ff 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -23,6 +23,10 @@
   color: $md-link-color;
 }
 
+.private-tokens-reset div.reset-action:not(:first-child) {
+  padding-top: 15px;
+}
+
 .oauth-buttons {
   .btn-group {
     margin-right: 10px;
@@ -77,14 +81,14 @@
 
 // Middle dot divider between each element in a list of items.
 .middle-dot-divider {
-  &:after {
+  &::after {
     content: "\00B7"; // Middle Dot
     padding: 0 6px;
     font-weight: bold;
   }
 
   &:last-child {
-    &:after {
+    &::after {
       content: "";
       padding: 0;
     }
@@ -93,8 +97,9 @@
 
 .profile-user-bio {
   // Limits the width of the user bio for readability.
-  max-width: 750px;
-  margin: auto;
+  max-width: 600px;
+  margin: 10px auto;
+  padding: 0 16px;
 }
 
 .user-avatar-button {
@@ -212,19 +217,48 @@
 }
 
 .user-profile {
+
+  .cover-controls a {
+    margin-left: 5px;
+  }
+
+  .profile-header {
+    margin: 0 auto;
+
+    .avatar-holder {
+      width: 90px;
+      margin: 0 auto 10px;
+    }
+  }
+
   @media (max-width: $screen-xs-max) {
+
     .cover-block {
       padding-top: 20px;
     }
 
     .cover-controls {
       position: static;
+      padding: 0 16px;
       margin-bottom: 20px;
+      display: -webkit-flex;
+      display: flex;
 
       .btn {
-        display: inline-block;
-        width: 46%;
+        -webkit-flex-grow: 1;
+        flex-grow: 1;
+
+        &:first-child {
+          margin-left: 0;
+        }
       }
     }
   }
 }
+
+table.u2f-registrations {
+  th:not(:last-child),
+  td:not(:last-child) {
+    border-right: solid 1px transparent;
+  }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index e5859fe7384acee05254a5bcadfdce3eb103b084..f8da0983b7709b6cb59a3dea01b04c133649cce3 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -4,7 +4,7 @@
     text-align: center;
 
     .preview {
-      @include border-radius(4px);
+      border-radius: 4px;
 
       height: 80px;
       margin-bottom: 10px;
@@ -47,7 +47,7 @@
       width: 160px;
 
       img {
-        @include border-radius(4px);
+        border-radius: 4px;
 
         max-width: 100%;
       }
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 27dc2b2a1fac30096abbe15e5a3d66e6e486a450..f7d5456453076c6b7ced775bc0e95e351557a49c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,56 +6,79 @@
   }
 }
 
-.no-ssh-key-message, .project-limit-message {
+.no-ssh-key-message,
+.project-limit-message {
   background-color: #f28d35;
   margin-bottom: 0;
 }
 
 .new_project,
 .edit-project {
+
   fieldset {
-    &.features .control-label {
-      font-weight: normal;
+
+    &.features {
+
+      .label-light {
+        margin-bottom: 0;
+      }
+
+      .help-block {
+        margin-top: 0;
+      }
     }
+
     .form-group {
       margin-bottom: 5px;
     }
+
     &> .form-group {
       padding-left: 0;
     }
   }
+
   .help-block {
     margin-bottom: 10px;
   }
+
   .project-path {
     padding-right: 0;
+
     .form-control {
       border-radius: $border-radius-base;
     }
   }
+
   .input-group > div {
+
     &:last-child {
       padding-right: 0;
     }
   }
+
   @media (max-width: $screen-xs-max) {
     .input-group > div {
+
       margin-bottom: 14px;
+
       &:last-child {
         margin-bottom: 0;
       }
     }
+
     fieldset > .form-group:first-child {
       padding-right: 0;
     }
   }
 
   .input-group-addon {
+
     &.static-namespace {
       height: 35px;
       border-radius: 3px;
       border: 1px solid #e5e5e5;
     }
+
     &+ .select2 a {
       border-top-left-radius: 0;
       border-bottom-left-radius: 0;
@@ -73,8 +96,8 @@
 
   .project-avatar {
     float: none;
-    margin-left: auto;
-    margin-right: auto;
+    margin: 0 auto;
+    border: none;
 
     &.identicon {
       border-radius: 50%;
@@ -146,7 +169,8 @@
   }
 
   .project-repo-btn-group,
-  .notification-dropdown {
+  .notification-dropdown,
+  .project-dropdown {
     margin-left: 10px;
   }
 
@@ -169,7 +193,7 @@
     margin-left: 4px;
 
     .arrow {
-      &:before {
+      &::before {
         content: '';
         display: inline-block;
         position: absolute;
@@ -185,7 +209,7 @@
         pointer-events: none;
       }
 
-      &:after {
+      &::after {
         content: '';
         position: absolute;
         width: 0;
@@ -200,6 +224,7 @@
         pointer-events: none;
       }
     }
+
     .count {
       @include btn-gray;
       display: inline-block;
@@ -311,6 +336,14 @@ a.deploy-project-label {
   color: $gl-success;
 }
 
+.lfs-enabled {
+  color: $gl-success;
+}
+
+.lfs-disabled {
+  color: $gl-warning;
+}
+
 .breadcrumb.repo-breadcrumb {
   padding: 0;
   background: transparent;
@@ -318,7 +351,7 @@ a.deploy-project-label {
   line-height: 36px;
   margin: 0;
 
-  > li + li:before {
+  > li + li::before {
     padding: 0 3px;
     color: #999;
   }
@@ -326,6 +359,10 @@ a.deploy-project-label {
   a {
     color: $gl-dark-link-color;
   }
+
+  .dropdown-menu {
+    width: 240px;
+  }
 }
 
 .last-push-widget {
@@ -341,35 +378,41 @@ a.deploy-project-label {
     justify-content: flex-start;
 
     .fork-thumbnail {
-      @include border-radius($border-radius-base);
+      border-radius: $border-radius-base;
       background-color: $white-light;
       border: 1px solid $border-white-light;
       height: 202px;
       margin: $gl-padding;
       text-align: center;
       width: 169px;
-      &:hover, &.forked {
+
+      &:hover,
+      &.forked {
         background-color: $row-hover;
         border-color: $row-hover-border;
       }
+
       .no-avatar {
         width: 100px;
         height: 100px;
         background-color: $gray-light;
         border: 1px solid $gray-dark;
         margin: 0 auto;
-        @include border-radius(50%);
+        border-radius: 50%;
+
         i {
           font-size: 100px;
           color: $gray-dark;
         }
       }
+
       a {
         display: block;
         width: 100%;
         height: 100%;
         padding-top: $gl-padding;
         color: $gl-gray;
+
         .caption {
           min-height: 30px;
           padding: $gl-padding 0;
@@ -377,7 +420,7 @@ a.deploy-project-label {
       }
 
       img {
-        @include border-radius(50%);
+        border-radius: 50%;
         max-width: 100px;
       }
     }
@@ -483,7 +526,7 @@ pre.light-well {
   }
 
   .light-well {
-    @include border-radius (2px);
+    border-radius: 2px;
 
     color: #5b6169;
     font-size: 13px;
@@ -600,18 +643,25 @@ pre.light-well {
   }
 }
 
-.project-show-readme .readme-holder {
-  padding: $gl-padding 0;
-  border-top: 0;
-
-  .edit-project-readme {
-    z-index: 2;
-    position: relative;
+.project-show-readme {
+  .row-content-block {
+    background-color: inherit;
+    border: none;
   }
 
-  .wiki h1 {
-    border-bottom: none;
-    padding: 0;
+  .readme-holder {
+    padding: $gl-padding 0;
+    border-top: 0;
+
+    .edit-project-readme {
+      z-index: 2;
+      position: relative;
+    }
+
+    .wiki h1 {
+      border-bottom: none;
+      padding: 0;
+    }
   }
 }
 
@@ -624,6 +674,7 @@ pre.light-well {
 
   .clone-options {
     display: table-cell;
+
     a.btn {
       width: 100%;
     }
@@ -685,7 +736,8 @@ pre.light-well {
   .table-bordered {
     border-radius: 1px;
 
-    th:not(:last-child), td:not(:last-child) {
+    th:not(:last-child),
+    td:not(:last-child) {
       border-right: solid 1px transparent;
     }
   }
@@ -708,9 +760,16 @@ pre.light-well {
   }
 }
 
-.project-refs-form {
-  .dropdown-menu {
-    width: 300px;
+.project-refs-form .dropdown-menu,
+.dropdown-menu-projects {
+  width: 300px;
+
+  @media (min-width: $screen-sm-min) {
+    width: 500px;
+  }
+
+  a {
+    white-space: normal;
   }
 }
 
@@ -719,3 +778,67 @@ pre.light-well {
     width: 300px;
   }
 }
+
+.clearable-input {
+  position: relative;
+
+  .clear-icon {
+    @extend .fa-times;
+    display: none;
+    position: absolute;
+    right: 7px;
+    top: 7px;
+    color: $location-icon-color;
+
+    &::before {
+      font-family: FontAwesome;
+      font-weight: normal;
+      font-style: normal;
+    }
+  }
+
+  &.has-value {
+    .clear-icon {
+      cursor: pointer;
+      display: block;
+    }
+  }
+}
+
+.project-path {
+  .form-control {
+    min-width: 100px;
+  }
+
+  .select2-choice {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+}
+
+.project-home-empty {
+  border-top: 0;
+
+  .container-fluid {
+    background: none;
+  }
+
+  p {
+    margin-left: auto;
+    margin-right: auto;
+    max-width: 650px;
+  }
+}
+
+.project-feature-nested {
+  @media (min-width: $screen-sm-min) {
+    padding-left: 45px;
+  }
+}
+
+.project-repo-select {
+  &.disabled {
+    opacity: 0.5;
+    pointer-events: none;
+  }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index eec22c5dc960384eb7caae00d6263a462afd4e51..7b3878c91df64a41764d44592ccd3ba25388c384 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -6,6 +6,7 @@
   &.runner-state-shared {
     background: #32b186;
   }
+
   &.runner-state-specific {
     background: #3498db;
   }
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index c9d436d72ba87cbb89f654b012413fd0c331b676..b4761df3f2335832c7d2ae6c9a276cad97fac446 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -31,7 +31,6 @@
     padding-right: 20px;
     border: none;
     font-size: 14px;
-    outline: none;
     padding: 0;
     margin-left: 5px;
     line-height: 25px;
@@ -65,13 +64,14 @@
   .search-input-wrap {
     width: 100%;
 
-    .search-icon, .clear-icon {
+    .search-icon,
+    .clear-icon {
       position: absolute;
       right: 5px;
       top: 0;
       color: $location-icon-color;
 
-      &:before {
+      &::before {
         font-family: FontAwesome;
         font-weight: normal;
         font-style: normal;
@@ -80,7 +80,7 @@
 
     .search-icon {
       @extend .fa-search;
-      @include transition(color .15s);
+      transition: color 0.15s;
       -webkit-user-select: none;
       -moz-user-select: none;
       -ms-user-select: none;
@@ -103,7 +103,7 @@
 
     // Custom dropdown positioning
     .dropdown-menu {
-      top: 30px;
+      top: 37px;
       left: -5px;
       padding: 0;
 
@@ -125,7 +125,7 @@
     }
 
     .location-badge {
-      @include transition(all .15s);
+      transition: all 0.15s;
       background-color: $location-badge-active-bg;
       color: $white-light;
     }
@@ -185,7 +185,8 @@
     padding-right: $gl-padding + 15px;
   }
 
-  .btn-search, .btn-new {
+  .btn-search,
+  .btn-new {
     width: 100%;
     margin-top: 5px;
 
@@ -227,6 +228,5 @@
   &:hover,
   &:focus {
     color: $gl-link-color;
-    outline: none;
   }
 }
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 2aa939b7dc389197b6b17e747ad9ac68617b82e3..857eb76131a9696c20a576b1b5f9e2bd7b91b368 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,20 +2,6 @@
   padding: 2px;
 }
 
-.snippet-holder {
-  margin-bottom: -$gl-padding;
-
-  .file-holder {
-    border-top: 0;
-  }
-
-  .file-actions {
-    .btn-clipboard {
-      @extend .btn;
-    }
-  }
-}
-
 .markdown-snippet-copy {
   position: fixed;
   top: -10px;
@@ -24,29 +10,25 @@
   max-width: 0;
 }
 
-.file-holder.snippet-file-content {
-  padding-bottom: $gl-padding;
-  border-bottom: 1px solid $border-color;
-
-  .file-title {
-    padding-top: $gl-padding;
-    padding-bottom: $gl-padding;
-  }
+.snippet-file-content {
+  border-radius: 3px;
+  margin-bottom: $gl-padding;
 
-  .file-actions {
-    top: 12px;
+  .btn-clipboard {
+    @extend .btn;
   }
+}
 
-  .file-content {
-    border-left: 1px solid $border-color;
-    border-right: 1px solid $border-color;
-    border-bottom: 1px solid $border-color;
-  }
+.project-snippets .awards {
+  border-bottom: 1px solid $table-border-color;
+  padding-bottom: $gl-padding;
 }
 
 .snippet-title {
   font-size: 24px;
-  font-weight: normal;
+  font-weight: 600;
+  padding: $gl-padding;
+  padding-left: 0;
 }
 
 .snippet-actions {
@@ -54,3 +36,7 @@
     float: right;
   }
 }
+
+.snippet-scope-menu .btn-new {
+  margin-top: 15px;
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 587f2d9f3c13735d1692b23561b0b0e507403f05..92997eae8b9fcac875bce2b161a3d039aac7237c 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -4,9 +4,10 @@
     margin-right: 10px;
     border: 1px solid #eee;
     white-space: nowrap;
-    @include border-radius(4px);
+    border-radius: 4px;
 
-    &:hover {
+    &:hover,
+    &:focus {
       text-decoration: none;
     }
 
@@ -43,6 +44,15 @@
       border-color: $blue-normal;
     }
 
+    &.ci-created {
+      color: $table-text-gray;
+      border-color: $table-text-gray;
+
+      svg {
+        fill: $table-text-gray;
+      }
+    }
+
     svg {
       height: 13px;
       width: 13px;
@@ -56,6 +66,7 @@
   .ci-status-icon-success {
     color: $gl-success;
   }
+
   .ci-status-icon-failed {
     color: $gl-danger;
   }
@@ -64,10 +75,11 @@
   .ci-status-icon-success_with_warning {
     color: $gl-warning;
   }
-  
+
   .ci-status-icon-running {
     color: $blue-normal;
   }
+
   .ci-status-icon-canceled,
   .ci-status-icon-disabled,
   .ci-status-icon-not-found,
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 0340526a53aa811859f3d9b696d2c0d862550bf1..b3aef2fdd328ecb7cfeaf8f4fe0ca3817790cd3d 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -51,6 +51,7 @@
     -webkit-flex-direction: column;
     flex-direction: column;
     margin-left: 10px;
+    min-width: 55px;
   }
 
   .todo-item {
@@ -99,7 +100,7 @@
 
       pre {
         border: none;
-        background: #f9f9f9;
+        background: $gray-light;
         border-radius: 0;
         color: #777;
         margin: 0 20px;
@@ -120,6 +121,14 @@
   }
 }
 
+@media (max-width: $screen-sm-max) {
+  .todos-filters {
+    .dropdown-menu-toggle {
+      width: 135px;
+    }
+  }
+}
+
 @media (max-width: $screen-xs-max) {
   .todo {
     .avatar {
@@ -141,4 +150,74 @@
       padding-left: 10px;
     }
   }
+
+  .todos-filters {
+    .row-content-block {
+      padding-bottom: 50px;
+    }
+
+    .dropdown-menu-toggle {
+      width: 100%;
+    }
+  }
+}
+
+.todos-empty {
+  display: -webkit-flex;
+  display: flex;
+  -webkit-flex-direction: column;
+  flex-direction: column;
+  max-width: 900px;
+  margin-left: auto;
+  margin-right: auto;
+
+  @media (min-width: $screen-sm-min) {
+    -webkit-flex-direction: row;
+    flex-direction: row;
+    padding-top: 80px;
+  }
+}
+
+.todos-empty-content {
+  -webkit-align-self: center;
+  align-self: center;
+  max-width: 480px;
+  margin-right: 20px;
+}
+
+.todos-empty-hero {
+  width: 200px;
+  margin-left: auto;
+  margin-right: auto;
+
+  @media (min-width: $screen-sm-min) {
+    width: 300px;
+    margin-right: 0;
+    -webkit-order: 2;
+    order: 2;
+  }
+}
+
+.todos-all-done {
+  padding-top: 20px;
+
+  @media (min-width: $screen-sm-min) {
+    padding-top: 50px;
+  }
+
+  > svg {
+    display: block;
+    max-width: 300px;
+    margin: 0 auto 20px;
+  }
+
+  p {
+    max-width: 470px;
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  a {
+    font-weight: 600;
+  }
 }
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 9da40fe2b09d5a6fac7b167defc56af175ee18ea..2b836fa1f4adeffa3c19a0866e9260b028abeb9e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -5,12 +5,17 @@
 
   .file-finder {
     width: 50%;
+
     .file-finder-input {
       width: 95%;
       display: inline-block;
     }
   }
 
+  .add-to-tree {
+    vertical-align: top;
+  }
+
   .tree-table {
     margin-bottom: 0;
 
@@ -18,10 +23,25 @@
       border-bottom: 1px solid $table-border-gray;
       border-top: 1px solid $table-border-gray;
 
-      td, th {
+      td,
+      th {
         line-height: 21px;
       }
 
+      .last-commit {
+        @include str-truncated(506px);
+
+        @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+          @include str-truncated(450px);
+        }
+
+      }
+
+      .commit-history-link-spacer {
+        margin: 0 10px;
+        color: $table-border-color;
+      }
+
       &:hover {
         td {
           background-color: $row-hover;
@@ -42,11 +62,21 @@
   }
 
   .tree-item {
+    .link-container {
+      padding: 0;
+
+      a {
+        padding: 10px $gl-padding;
+        display: block;
+      }
+    }
+
     .tree-item-file-name {
       max-width: 320px;
       vertical-align: middle;
 
-      i, a {
+      i,
+      a {
         color: $gl-dark-link-color;
       }
 
@@ -77,11 +107,17 @@
     }
   }
 
-  .tree_commit {
-    color: $gl-gray;
+  .tree-time-ago {
+    min-width: 135px;
+    color: $gl-gray-light;
+  }
+
+  .tree-commit {
+    max-width: 320px;
+    color: $gl-gray-light;
 
     .tree-commit-link {
-      color: $gl-gray;
+      color: $gl-gray-light;
 
       &:hover {
         text-decoration: underline;
@@ -135,4 +171,8 @@
   margin-top: 11px;
   position: relative;
   z-index: 2;
+
+  .download-button {
+    margin-left: $btn-side-margin;
+  }
 }
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 587bd6a1e8a8fd882e291275ce4837b7faf87621..e73cecc92be3a998f2348dcc260a41bc0296d6c3 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -5,7 +5,7 @@
   }
 
   .example {
-    &:before {
+    &::before {
       content: "Example";
       color: #bbb;
     }
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 8d855ce99b021239eccac3ade77923b1132081e8..3fa7fa3d7e3fc47bfe63137c7d5e86549c92a03c 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -20,15 +20,22 @@
   $l-cyan: #8abeb7;
   $l-white: $ci-text-color;
 
+  .term-bold {
+    font-weight: bold;
+  }
+
   .term-italic {
     font-style: italic;
   }
+
   .term-conceal {
     visibility: hidden;
   }
+
   .term-underline {
     text-decoration: underline;
   }
+
   .term-cross {
     text-decoration: line-through;
   }
@@ -36,48 +43,63 @@
   .term-fg-black {
     color: $black;
   }
+
   .term-fg-red {
     color: $red;
   }
+
   .term-fg-green {
     color: $green;
   }
+
   .term-fg-yellow {
     color: $yellow;
   }
+
   .term-fg-blue {
     color: $blue;
   }
+
   .term-fg-magenta {
     color: $magenta;
   }
+
   .term-fg-cyan {
     color: $cyan;
   }
+
   .term-fg-white {
     color: $white;
   }
+
   .term-fg-l-black {
     color: $l-black;
   }
+
   .term-fg-l-red {
     color: $l-red;
   }
+
   .term-fg-l-green {
     color: $l-green;
   }
+
   .term-fg-l-yellow {
     color: $l-yellow;
   }
+
   .term-fg-l-blue {
     color: $l-blue;
   }
+
   .term-fg-l-magenta {
     color: $l-magenta;
   }
+
   .term-fg-l-cyan {
     color: $l-cyan;
   }
+
   .term-fg-l-white {
     color: $l-white;
   }
@@ -85,818 +107,1087 @@
   .term-bg-black {
     background-color: $black;
   }
+
   .term-bg-red {
     background-color: $red;
   }
+
   .term-bg-green {
     background-color: $green;
   }
+
   .term-bg-yellow {
     background-color: $yellow;
   }
+
   .term-bg-blue {
     background-color: $blue;
   }
+
   .term-bg-magenta {
     background-color: $magenta;
   }
+
   .term-bg-cyan {
     background-color: $cyan;
   }
+
   .term-bg-white {
     background-color: $white;
   }
+
   .term-bg-l-black {
     background-color: $l-black;
   }
+
   .term-bg-l-red {
     background-color: $l-red;
   }
+
   .term-bg-l-green {
     background-color: $l-green;
   }
+
   .term-bg-l-yellow {
     background-color: $l-yellow;
   }
+
   .term-bg-l-blue {
     background-color: $l-blue;
   }
+
   .term-bg-l-magenta {
     background-color: $l-magenta;
   }
+
   .term-bg-l-cyan {
     background-color: $l-cyan;
   }
+
   .term-bg-l-white {
     background-color: $l-white;
   }
 
-
   .xterm-fg-0 {
     color: #000;
   }
+
   .xterm-fg-1 {
     color: #800000;
   }
+
   .xterm-fg-2 {
     color: #008000;
   }
+
   .xterm-fg-3 {
     color: #808000;
   }
+
   .xterm-fg-4 {
     color: #000080;
   }
+
   .xterm-fg-5 {
     color: #800080;
   }
+
   .xterm-fg-6 {
     color: #008080;
   }
+
   .xterm-fg-7 {
     color: #c0c0c0;
   }
+
   .xterm-fg-8 {
     color: #808080;
   }
+
   .xterm-fg-9 {
     color: #f00;
   }
+
   .xterm-fg-10 {
     color: #0f0;
   }
+
   .xterm-fg-11 {
     color: #ff0;
   }
+
   .xterm-fg-12 {
     color: #00f;
   }
+
   .xterm-fg-13 {
     color: #f0f;
   }
+
   .xterm-fg-14 {
     color: #0ff;
   }
+
   .xterm-fg-15 {
     color: #fff;
   }
+
   .xterm-fg-16 {
     color: #000;
   }
+
   .xterm-fg-17 {
     color: #00005f;
   }
+
   .xterm-fg-18 {
     color: #000087;
   }
+
   .xterm-fg-19 {
     color: #0000af;
   }
+
   .xterm-fg-20 {
     color: #0000d7;
   }
+
   .xterm-fg-21 {
     color: #00f;
   }
+
   .xterm-fg-22 {
     color: #005f00;
   }
+
   .xterm-fg-23 {
     color: #005f5f;
   }
+
   .xterm-fg-24 {
     color: #005f87;
   }
+
   .xterm-fg-25 {
     color: #005faf;
   }
+
   .xterm-fg-26 {
     color: #005fd7;
   }
+
   .xterm-fg-27 {
     color: #005fff;
   }
+
   .xterm-fg-28 {
     color: #008700;
   }
+
   .xterm-fg-29 {
     color: #00875f;
   }
+
   .xterm-fg-30 {
     color: #008787;
   }
+
   .xterm-fg-31 {
     color: #0087af;
   }
+
   .xterm-fg-32 {
     color: #0087d7;
   }
+
   .xterm-fg-33 {
     color: #0087ff;
   }
+
   .xterm-fg-34 {
     color: #00af00;
   }
+
   .xterm-fg-35 {
     color: #00af5f;
   }
+
   .xterm-fg-36 {
     color: #00af87;
   }
+
   .xterm-fg-37 {
     color: #00afaf;
   }
+
   .xterm-fg-38 {
     color: #00afd7;
   }
+
   .xterm-fg-39 {
     color: #00afff;
   }
+
   .xterm-fg-40 {
     color: #00d700;
   }
+
   .xterm-fg-41 {
     color: #00d75f;
   }
+
   .xterm-fg-42 {
     color: #00d787;
   }
+
   .xterm-fg-43 {
     color: #00d7af;
   }
+
   .xterm-fg-44 {
     color: #00d7d7;
   }
+
   .xterm-fg-45 {
     color: #00d7ff;
   }
+
   .xterm-fg-46 {
     color: #0f0;
   }
+
   .xterm-fg-47 {
     color: #00ff5f;
   }
+
   .xterm-fg-48 {
     color: #00ff87;
   }
+
   .xterm-fg-49 {
     color: #00ffaf;
   }
+
   .xterm-fg-50 {
     color: #00ffd7;
   }
+
   .xterm-fg-51 {
     color: #0ff;
   }
+
   .xterm-fg-52 {
     color: #5f0000;
   }
+
   .xterm-fg-53 {
     color: #5f005f;
   }
+
   .xterm-fg-54 {
     color: #5f0087;
   }
+
   .xterm-fg-55 {
     color: #5f00af;
   }
+
   .xterm-fg-56 {
     color: #5f00d7;
   }
+
   .xterm-fg-57 {
     color: #5f00ff;
   }
+
   .xterm-fg-58 {
     color: #5f5f00;
   }
+
   .xterm-fg-59 {
     color: #5f5f5f;
   }
+
   .xterm-fg-60 {
     color: #5f5f87;
   }
+
   .xterm-fg-61 {
     color: #5f5faf;
   }
+
   .xterm-fg-62 {
     color: #5f5fd7;
   }
+
   .xterm-fg-63 {
     color: #5f5fff;
   }
+
   .xterm-fg-64 {
     color: #5f8700;
   }
+
   .xterm-fg-65 {
     color: #5f875f;
   }
+
   .xterm-fg-66 {
     color: #5f8787;
   }
+
   .xterm-fg-67 {
     color: #5f87af;
   }
+
   .xterm-fg-68 {
     color: #5f87d7;
   }
+
   .xterm-fg-69 {
     color: #5f87ff;
   }
+
   .xterm-fg-70 {
     color: #5faf00;
   }
+
   .xterm-fg-71 {
     color: #5faf5f;
   }
+
   .xterm-fg-72 {
     color: #5faf87;
   }
+
   .xterm-fg-73 {
     color: #5fafaf;
   }
+
   .xterm-fg-74 {
     color: #5fafd7;
   }
+
   .xterm-fg-75 {
     color: #5fafff;
   }
+
   .xterm-fg-76 {
     color: #5fd700;
   }
+
   .xterm-fg-77 {
     color: #5fd75f;
   }
+
   .xterm-fg-78 {
     color: #5fd787;
   }
+
   .xterm-fg-79 {
     color: #5fd7af;
   }
+
   .xterm-fg-80 {
     color: #5fd7d7;
   }
+
   .xterm-fg-81 {
     color: #5fd7ff;
   }
+
   .xterm-fg-82 {
     color: #5fff00;
   }
+
   .xterm-fg-83 {
     color: #5fff5f;
   }
+
   .xterm-fg-84 {
     color: #5fff87;
   }
+
   .xterm-fg-85 {
     color: #5fffaf;
   }
+
   .xterm-fg-86 {
     color: #5fffd7;
   }
+
   .xterm-fg-87 {
     color: #5fffff;
   }
+
   .xterm-fg-88 {
     color: #870000;
   }
+
   .xterm-fg-89 {
     color: #87005f;
   }
+
   .xterm-fg-90 {
     color: #870087;
   }
+
   .xterm-fg-91 {
     color: #8700af;
   }
+
   .xterm-fg-92 {
     color: #8700d7;
   }
+
   .xterm-fg-93 {
     color: #8700ff;
   }
+
   .xterm-fg-94 {
     color: #875f00;
   }
+
   .xterm-fg-95 {
     color: #875f5f;
   }
+
   .xterm-fg-96 {
     color: #875f87;
   }
+
   .xterm-fg-97 {
     color: #875faf;
   }
+
   .xterm-fg-98 {
     color: #875fd7;
   }
+
   .xterm-fg-99 {
     color: #875fff;
   }
+
   .xterm-fg-100 {
     color: #878700;
   }
+
   .xterm-fg-101 {
     color: #87875f;
   }
+
   .xterm-fg-102 {
     color: #878787;
   }
+
   .xterm-fg-103 {
     color: #8787af;
   }
+
   .xterm-fg-104 {
     color: #8787d7;
   }
+
   .xterm-fg-105 {
     color: #8787ff;
   }
+
   .xterm-fg-106 {
     color: #87af00;
   }
+
   .xterm-fg-107 {
     color: #87af5f;
   }
+
   .xterm-fg-108 {
     color: #87af87;
   }
+
   .xterm-fg-109 {
     color: #87afaf;
   }
+
   .xterm-fg-110 {
     color: #87afd7;
   }
+
   .xterm-fg-111 {
     color: #87afff;
   }
+
   .xterm-fg-112 {
     color: #87d700;
   }
+
   .xterm-fg-113 {
     color: #87d75f;
   }
+
   .xterm-fg-114 {
     color: #87d787;
   }
+
   .xterm-fg-115 {
     color: #87d7af;
   }
+
   .xterm-fg-116 {
     color: #87d7d7;
   }
+
   .xterm-fg-117 {
     color: #87d7ff;
   }
+
   .xterm-fg-118 {
     color: #87ff00;
   }
+
   .xterm-fg-119 {
     color: #87ff5f;
   }
+
   .xterm-fg-120 {
     color: #87ff87;
   }
+
   .xterm-fg-121 {
     color: #87ffaf;
   }
+
   .xterm-fg-122 {
     color: #87ffd7;
   }
+
   .xterm-fg-123 {
     color: #87ffff;
   }
+
   .xterm-fg-124 {
     color: #af0000;
   }
+
   .xterm-fg-125 {
     color: #af005f;
   }
+
   .xterm-fg-126 {
     color: #af0087;
   }
+
   .xterm-fg-127 {
     color: #af00af;
   }
+
   .xterm-fg-128 {
     color: #af00d7;
   }
+
   .xterm-fg-129 {
     color: #af00ff;
   }
+
   .xterm-fg-130 {
     color: #af5f00;
   }
+
   .xterm-fg-131 {
     color: #af5f5f;
   }
+
   .xterm-fg-132 {
     color: #af5f87;
   }
+
   .xterm-fg-133 {
     color: #af5faf;
   }
+
   .xterm-fg-134 {
     color: #af5fd7;
   }
+
   .xterm-fg-135 {
     color: #af5fff;
   }
+
   .xterm-fg-136 {
     color: #af8700;
   }
+
   .xterm-fg-137 {
     color: #af875f;
   }
+
   .xterm-fg-138 {
     color: #af8787;
   }
+
   .xterm-fg-139 {
     color: #af87af;
   }
+
   .xterm-fg-140 {
     color: #af87d7;
   }
+
   .xterm-fg-141 {
     color: #af87ff;
   }
+
   .xterm-fg-142 {
     color: #afaf00;
   }
+
   .xterm-fg-143 {
     color: #afaf5f;
   }
+
   .xterm-fg-144 {
     color: #afaf87;
   }
+
   .xterm-fg-145 {
     color: #afafaf;
   }
+
   .xterm-fg-146 {
     color: #afafd7;
   }
+
   .xterm-fg-147 {
     color: #afafff;
   }
+
   .xterm-fg-148 {
     color: #afd700;
   }
+
   .xterm-fg-149 {
     color: #afd75f;
   }
+
   .xterm-fg-150 {
     color: #afd787;
   }
+
   .xterm-fg-151 {
     color: #afd7af;
   }
+
   .xterm-fg-152 {
     color: #afd7d7;
   }
+
   .xterm-fg-153 {
     color: #afd7ff;
   }
+
   .xterm-fg-154 {
     color: #afff00;
   }
+
   .xterm-fg-155 {
     color: #afff5f;
   }
+
   .xterm-fg-156 {
     color: #afff87;
   }
+
   .xterm-fg-157 {
     color: #afffaf;
   }
+
   .xterm-fg-158 {
     color: #afffd7;
   }
+
   .xterm-fg-159 {
     color: #afffff;
   }
+
   .xterm-fg-160 {
     color: #d70000;
   }
+
   .xterm-fg-161 {
     color: #d7005f;
   }
+
   .xterm-fg-162 {
     color: #d70087;
   }
+
   .xterm-fg-163 {
     color: #d700af;
   }
+
   .xterm-fg-164 {
     color: #d700d7;
   }
+
   .xterm-fg-165 {
     color: #d700ff;
   }
+
   .xterm-fg-166 {
     color: #d75f00;
   }
+
   .xterm-fg-167 {
     color: #d75f5f;
   }
+
   .xterm-fg-168 {
     color: #d75f87;
   }
+
   .xterm-fg-169 {
     color: #d75faf;
   }
+
   .xterm-fg-170 {
     color: #d75fd7;
   }
+
   .xterm-fg-171 {
     color: #d75fff;
   }
+
   .xterm-fg-172 {
     color: #d78700;
   }
+
   .xterm-fg-173 {
     color: #d7875f;
   }
+
   .xterm-fg-174 {
     color: #d78787;
   }
+
   .xterm-fg-175 {
     color: #d787af;
   }
+
   .xterm-fg-176 {
     color: #d787d7;
   }
+
   .xterm-fg-177 {
     color: #d787ff;
   }
+
   .xterm-fg-178 {
     color: #d7af00;
   }
+
   .xterm-fg-179 {
     color: #d7af5f;
   }
+
   .xterm-fg-180 {
     color: #d7af87;
   }
+
   .xterm-fg-181 {
     color: #d7afaf;
   }
+
   .xterm-fg-182 {
     color: #d7afd7;
   }
+
   .xterm-fg-183 {
     color: #d7afff;
   }
+
   .xterm-fg-184 {
     color: #d7d700;
   }
+
   .xterm-fg-185 {
     color: #d7d75f;
   }
+
   .xterm-fg-186 {
     color: #d7d787;
   }
+
   .xterm-fg-187 {
     color: #d7d7af;
   }
+
   .xterm-fg-188 {
     color: #d7d7d7;
   }
+
   .xterm-fg-189 {
     color: #d7d7ff;
   }
+
   .xterm-fg-190 {
     color: #d7ff00;
   }
+
   .xterm-fg-191 {
     color: #d7ff5f;
   }
+
   .xterm-fg-192 {
     color: #d7ff87;
   }
+
   .xterm-fg-193 {
     color: #d7ffaf;
   }
+
   .xterm-fg-194 {
     color: #d7ffd7;
   }
+
   .xterm-fg-195 {
     color: #d7ffff;
   }
+
   .xterm-fg-196 {
     color: #f00;
   }
+
   .xterm-fg-197 {
     color: #ff005f;
   }
+
   .xterm-fg-198 {
     color: #ff0087;
   }
+
   .xterm-fg-199 {
     color: #ff00af;
   }
+
   .xterm-fg-200 {
     color: #ff00d7;
   }
+
   .xterm-fg-201 {
     color: #f0f;
   }
+
   .xterm-fg-202 {
     color: #ff5f00;
   }
+
   .xterm-fg-203 {
     color: #ff5f5f;
   }
+
   .xterm-fg-204 {
     color: #ff5f87;
   }
+
   .xterm-fg-205 {
     color: #ff5faf;
   }
+
   .xterm-fg-206 {
     color: #ff5fd7;
   }
+
   .xterm-fg-207 {
     color: #ff5fff;
   }
+
   .xterm-fg-208 {
     color: #ff8700;
   }
+
   .xterm-fg-209 {
     color: #ff875f;
   }
+
   .xterm-fg-210 {
     color: #ff8787;
   }
+
   .xterm-fg-211 {
     color: #ff87af;
   }
+
   .xterm-fg-212 {
     color: #ff87d7;
   }
+
   .xterm-fg-213 {
     color: #ff87ff;
   }
+
   .xterm-fg-214 {
     color: #ffaf00;
   }
+
   .xterm-fg-215 {
     color: #ffaf5f;
   }
+
   .xterm-fg-216 {
     color: #ffaf87;
   }
+
   .xterm-fg-217 {
     color: #ffafaf;
   }
+
   .xterm-fg-218 {
     color: #ffafd7;
   }
+
   .xterm-fg-219 {
     color: #ffafff;
   }
+
   .xterm-fg-220 {
     color: #ffd700;
   }
+
   .xterm-fg-221 {
     color: #ffd75f;
   }
+
   .xterm-fg-222 {
     color: #ffd787;
   }
+
   .xterm-fg-223 {
     color: #ffd7af;
   }
+
   .xterm-fg-224 {
     color: #ffd7d7;
   }
+
   .xterm-fg-225 {
     color: #ffd7ff;
   }
+
   .xterm-fg-226 {
     color: #ff0;
   }
+
   .xterm-fg-227 {
     color: #ffff5f;
   }
+
   .xterm-fg-228 {
     color: #ffff87;
   }
+
   .xterm-fg-229 {
     color: #ffffaf;
   }
+
   .xterm-fg-230 {
     color: #ffffd7;
   }
+
   .xterm-fg-231 {
     color: #fff;
   }
+
   .xterm-fg-232 {
     color: #080808;
   }
+
   .xterm-fg-233 {
     color: #121212;
   }
+
   .xterm-fg-234 {
     color: #1c1c1c;
   }
+
   .xterm-fg-235 {
     color: #262626;
   }
+
   .xterm-fg-236 {
     color: #303030;
   }
+
   .xterm-fg-237 {
     color: #3a3a3a;
   }
+
   .xterm-fg-238 {
     color: #444;
   }
+
   .xterm-fg-239 {
     color: #4e4e4e;
   }
+
   .xterm-fg-240 {
     color: #585858;
   }
+
   .xterm-fg-241 {
     color: #626262;
   }
+
   .xterm-fg-242 {
     color: #6c6c6c;
   }
+
   .xterm-fg-243 {
     color: #767676;
   }
+
   .xterm-fg-244 {
     color: #808080;
   }
+
   .xterm-fg-245 {
     color: #8a8a8a;
   }
+
   .xterm-fg-246 {
     color: #949494;
   }
+
   .xterm-fg-247 {
     color: #9e9e9e;
   }
+
   .xterm-fg-248 {
     color: #a8a8a8;
   }
+
   .xterm-fg-249 {
     color: #b2b2b2;
   }
+
   .xterm-fg-250 {
     color: #bcbcbc;
   }
+
   .xterm-fg-251 {
     color: #c6c6c6;
   }
+
   .xterm-fg-252 {
     color: #d0d0d0;
   }
+
   .xterm-fg-253 {
     color: #dadada;
   }
+
   .xterm-fg-254 {
     color: #e4e4e4;
   }
+
   .xterm-fg-255 {
     color: #eee;
   }
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index a30b64925729a620cded078f4ff983fd566b2740..0ff3c3f547290b97a58da0d6113e2fa581c5a1ed 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -1,7 +1,24 @@
-.wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; }
-.wiki h1 {font-size: 30px;}
-.wiki h2 {font-size: 22px;}
-.wiki h3 {font-size: 18px; font-weight: bold; }
+.wiki h1,
+.wiki h2,
+.wiki h3,
+.wiki h4,
+.wiki h5,
+.wiki h6 {
+  margin-top: 17px;
+}
+
+.wiki h1 {
+  font-size: 30px;
+}
+
+.wiki h2 {
+  font-size: 22px;
+}
+
+.wiki h3 {
+  font-size: 18px;
+  font-weight: bold;
+}
 
 header,
 nav,
@@ -18,7 +35,7 @@ nav.navbar-collapse.collapse,
 .nav,
 .btn,
 ul.notes-form,
-.merge-request-ci-status .ci-status-link:after,
+.merge-request-ci-status .ci-status-link::after,
 .issuable-gutter-toggle,
 .gutter-toggle,
 .issuable-details .content-block-small,
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9e1dc15de849c53075870720aab4668a9a5c1d56..52e0256943acdf1ec7a92d4201d76dd18b7f119a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,13 +109,20 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
       :sentry_dsn,
       :akismet_enabled,
       :akismet_api_key,
+      :koding_enabled,
+      :koding_url,
       :email_author_in_body,
       :repository_checks_enabled,
       :metrics_packet_size,
       :send_user_confirmation_email,
       :container_registry_token_expire_delay,
-      :repository_storage,
       :enabled_git_access_protocol,
+      :housekeeping_enabled,
+      :housekeeping_bitmaps_enabled,
+      :housekeeping_incremental_repack_period,
+      :housekeeping_full_repack_period,
+      :housekeeping_gc_period,
+      repository_storages: [],
       restricted_visibility_levels: [],
       import_sources: [],
       disabled_oauth_sign_in_sources: []
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 82055006ac0ef6b62966a75d6c4b5fad375cedbe..762e36ee2e961a26765bc858c49c8bf780227ea0 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
   end
 
   def preview
-    @message = broadcast_message_params[:message]
+    @broadcast_message = BroadcastMessage.new(broadcast_message_params)
   end
 
   protected
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4ce18321649dac629f511ff1b6789d7a34aa97b6..aa7570cd896836419652a2db635a770f28545830 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
 
   def show
     @members = @group.members.order("access_level DESC").page(params[:members_page])
-    @requesters = @group.requesters
+    @requesters = AccessRequestsFinder.new(@group).execute(current_user)
     @projects = @group.projects.page(params[:projects_page])
   end
 
@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
   end
 
   def members_update
-    @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+    @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
 
     redirect_to [:admin, @group], notice: 'Users were successfully added.'
   end
@@ -60,6 +60,14 @@ class Admin::GroupsController < Admin::ApplicationController
   end
 
   def group_params
-    params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
+    params.require(:group).permit(
+      :avatar,
+      :description,
+      :lfs_enabled,
+      :name,
+      :path,
+      :request_access_enabled,
+      :visibility_level
+    )
   end
 end
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 8be35f00a775f70e27ef584d56d1993477c42b6c..9433da02f646deac3323fcb366d15e9595b2e003 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -7,7 +7,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
 
     warden.set_user(impersonator, scope: :user)
 
-    Gitlab::AppLogger.info("User #{original_user.username} has stopped impersonating #{impersonator.username}")
+    Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
 
     session[:impersonator_id] = nil
 
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 0d2f4f6eb384d133812610abfb5f04829342a864..1d963bdf7d58df28917edef167ac5141a5e52458 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController
     end
 
     @project_members = @project.members.page(params[:project_members_page])
-    @requesters = @project.requesters
+    @requesters = AccessRequestsFinder.new(@project).execute(current_user)
   end
 
   def transfer
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 7c37f3155dac8f0435bd02ac297a505f0b1d15da..37a1a23178eb78f26d214322b893a9fde5f04d91 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
   private
 
   def services_templates
-    templates = []
-
-    Service.available_services_names.each do |service_name|
+    Service.available_services_names.map do |service_name|
       service_template = service_name.concat("_service").camelize.constantize
-      templates << service_template.where(template: true).first_or_create
+      service_template.where(template: true).first_or_create
     end
-
-    templates
   end
 
   def service
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index e4c730088269169b3b5b3953d77e1e540b6776f7..ca04a17caa14b8b395b53ed1201beda33c1e79ae 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -29,7 +29,8 @@ class Admin::SystemInfoController < Admin::ApplicationController
   ]
 
   def show
-    system_info = Vmstat.snapshot
+    @cpus = Vmstat.cpu rescue nil
+    @memory = Vmstat.memory rescue nil
     mounts = Sys::Filesystem.mounts
 
     @disks = []
@@ -50,10 +51,5 @@ class Admin::SystemInfoController < Admin::ApplicationController
       rescue Sys::Filesystem::Error
       end
     end
-
-    @cpus = system_info.cpus.length
-
-    @mem_used = system_info.memory.active_bytes
-    @mem_total = system_info.memory.total_bytes
   end
 end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f35f4a8c8112552dc8ac4fe3df822066f6735b23..bb912ed10cca4a264e8b64ca54036e14cd341ded 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -3,7 +3,7 @@ class Admin::UsersController < Admin::ApplicationController
 
   def index
     @users = User.order_name_asc.filter(params[:filter])
-    @users = @users.search(params[:name]) if params[:name].present?
+    @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
     @users = @users.sort(@sort = params[:sort])
     @users = @users.page(params[:page])
   end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 634d36a44671ed450597061fcbb3f6bd9f9903fa..517ad4f03f33d35039dbf767cc9efd703afc55a3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
   include Gitlab::GonHelper
   include GitlabRoutingHelper
   include PageLayoutHelper
+  include SentryHelper
   include WorkhorseHelper
 
   before_action :authenticate_user_from_private_token!
@@ -23,8 +24,8 @@ class ApplicationController < ActionController::Base
 
   protect_from_forgery with: :exception
 
-  helper_method :abilities, :can?, :current_application_settings
-  helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+  helper_method :can?, :current_application_settings
+  helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
 
   rescue_from Encoding::CompatibilityError do |exception|
     log_exception(exception)
@@ -44,29 +45,11 @@ class ApplicationController < ActionController::Base
     redirect_to request.referer.present? ? :back : default, options
   end
 
-  protected
-
-  def sentry_context
-    if Rails.env.production? && current_application_settings.sentry_enabled
-      if current_user
-        Raven.user_context(
-          id: current_user.id,
-          email: current_user.email,
-          username: current_user.username,
-        )
-      end
-
-      Raven.tags_context(program: sentry_program_context)
-    end
+  def not_found
+    render_404
   end
 
-  def sentry_program_context
-    if Sidekiq.server?
-      'sidekiq'
-    else
-      'rails'
-    end
-  end
+  protected
 
   # This filter handles both private tokens and personal access tokens
   def authenticate_user_from_private_token!
@@ -118,12 +101,8 @@ class ApplicationController < ActionController::Base
     current_application_settings.after_sign_out_path.presence || new_user_session_path
   end
 
-  def abilities
-    Ability.abilities
-  end
-
   def can?(object, action, subject)
-    abilities.allowed?(object, action, subject)
+    Ability.allowed?(object, action, subject)
   end
 
   def access_denied!
@@ -139,7 +118,12 @@ class ApplicationController < ActionController::Base
   end
 
   def render_404
-    render file: Rails.root.join("public", "404"), layout: false, status: "404"
+    respond_to do |format|
+      format.html do
+        render file: Rails.root.join("public", "404"), layout: false, status: "404"
+      end
+      format.any { head :not_found }
+    end
   end
 
   def no_cache_headers
@@ -198,7 +182,8 @@ class ApplicationController < ActionController::Base
   end
 
   def event_filter
-    filters = cookies['event_filter'].split(',') if cookies['event_filter'].present?
+    # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
+    filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
     @event_filter ||= EventFilter.new(filters)
   end
 
@@ -207,9 +192,10 @@ class ApplicationController < ActionController::Base
   end
 
   # JSON for infinite scroll via Pager object
-  def pager_json(partial, count)
+  def pager_json(partial, count, locals = {})
     html = render_to_string(
       partial,
+      locals: locals,
       layout: false,
       formats: [:html]
     )
@@ -271,10 +257,6 @@ class ApplicationController < ActionController::Base
     Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
   end
 
-  def gitorious_import_enabled?
-    current_application_settings.import_sources.include?('gitorious')
-  end
-
   def google_code_import_enabled?
     current_application_settings.import_sources.include?('google_code')
   end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index b48668eea87295385631f753ef973086e207b2ea..daa82336208c47109a1e297f73789e50206bc851 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -11,9 +11,13 @@ class AutocompleteController < ApplicationController
     @users = @users.reorder(:name)
     @users = @users.page(params[:page])
 
+    if params[:todo_filter].present?
+      @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
+    end
+
     if params[:search].blank?
       # Include current user if available to filter by "Me"
-      if params[:current_user] && current_user
+      if params[:current_user].present? && current_user
         @users = [*@users, current_user]
       end
 
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
deleted file mode 100644
index 5bb7d499cdc5182fc8b988fa03008c4ec3930021..0000000000000000000000000000000000000000
--- a/app/controllers/ci/application_controller.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Ci
-  class ApplicationController < ::ApplicationController
-    def self.railtie_helpers_paths
-      "app/helpers/ci"
-    end
-  end
-end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index a7af3cb83450b1e6d5fc23559345d7f4accee53f..3eb485de9db8d11f44afb23cd995363c523522d8 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -1,5 +1,5 @@
 module Ci
-  class LintsController < ApplicationController
+  class LintsController < ::ApplicationController
     before_action :authenticate_user!
 
     def show
@@ -7,19 +7,15 @@ module Ci
 
     def create
       @content = params[:content]
+      @error = Ci::GitlabCiYamlProcessor.validation_message(@content)
+      @status = @error.blank?
 
-      if @content.blank?
-        @status = false
-        @error = "Please provide content of .gitlab-ci.yml"
-      else
+      if @error.blank?
         @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
         @stages = @config_processor.stages
         @builds = @config_processor.builds
-        @status = true
+        @jobs = @config_processor.jobs
       end
-    rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
-      @error = e.message
-      @status = false
     rescue
       @error = 'Undefined error'
       @status = false
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index aa894fde36b8fc978b0aeeb311c48618d8fd10fd..ff297d6ff13143b1b0f762e056eb67d090d72161 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,5 +1,5 @@
 module Ci
-  class ProjectsController < Ci::ApplicationController
+  class ProjectsController < ::ApplicationController
     before_action :project
     before_action :no_cache, only: [:badge]
     before_action :authorize_read_project!, except: [:badge, :index]
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index ba07cea569c870111ae7b623aa157732c5d87eb3..4c497711fc00a1710e586193ba2083b65dc52b65 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
   #
   # Returns nil
   def prompt_for_two_factor(user)
+    return locked_user_redirect(user) if user.access_locked?
+
     session[:otp_user_id] = user.id
     setup_u2f_authentication(user)
     render 'devise/sessions/two_factor'
   end
 
+  def locked_user_redirect(user)
+    flash.now[:alert] = 'Invalid Login or password'
+    render 'devise/sessions/new'
+  end
+
   def authenticate_with_two_factor
     user = self.resource = find_user
 
-    if user_params[:otp_attempt].present? && session[:otp_user_id]
+    if user.access_locked?
+      locked_user_redirect(user)
+    elsif 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)
@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
       remember_me(user) if user_params[:remember_me] == '1'
       sign_in(user)
     else
+      user.increment_failed_attempts!
       flash.now[:alert] = 'Invalid two-factor code.'
-      render :two_factor
+      prompt_for_two_factor(user)
     end
   end
 
@@ -62,8 +72,10 @@ module AuthenticatesWithTwoFactor
       session.delete(:otp_user_id)
       session.delete(:challenges)
 
+      remember_me(user) if user_params[:remember_me] == '1'
       sign_in(user)
     else
+      user.increment_failed_attempts!
       flash.now[:alert] = 'Authentication via U2F device failed.'
       prompt_for_two_factor(user)
     end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index f2b8f297bc27886a536478ba77aeb82304cfaa56..dacb5679dd300fde4a034ba762960ee5a41610e7 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -7,8 +7,7 @@ module CreatesCommit
     commit_params = @commit_params.merge(
       source_project: @project,
       source_branch: @ref,
-      target_branch: @target_branch,
-      previous_path: @previous_path
+      target_branch: @target_branch
     )
 
     result = service.new(@tree_edit_project, current_user, commit_params).execute
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index f40b62446e5ecbd223e5c7b282e25a4d9c64d31e..be86fa106f8b4df8793c1bfcbd7e305d8644b6f0 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -2,22 +2,60 @@ module IssuableActions
   extend ActiveSupport::Concern
 
   included do
+    before_action :labels, only: [:show, :new, :edit]
     before_action :authorize_destroy_issuable!, only: :destroy
+    before_action :authorize_admin_issuable!, only: :bulk_update
   end
 
   def destroy
     issuable.destroy
+    destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
+    TodoService.new.public_send(destroy_method, issuable, current_user)
 
     name = issuable.class.name.titleize.downcase
     flash[:notice] = "The #{name} was successfully deleted."
     redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
   end
 
+  def bulk_update
+    result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
+    quantity = result[:count]
+
+    render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
+  end
+
   private
 
+  def labels
+    @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+  end
+
   def authorize_destroy_issuable!
-    unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+    unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
       return access_denied!
     end
   end
+
+  def authorize_admin_issuable!
+    unless can?(current_user, :"admin_#{resource_name}", @project)
+      return access_denied!
+    end
+  end
+
+  def bulk_update_params
+    params.require(:update).permit(
+      :issuable_ids,
+      :assignee_id,
+      :milestone_id,
+      :state_event,
+      :subscription_event,
+      label_ids: [],
+      add_label_ids: [],
+      remove_label_ids: []
+    )
+  end
+
+  def resource_name
+    @resource_name ||= controller_name.singularize
+  end
 end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index c802922e0af83c1b80728fac462e7111861ffd2c..b5e79099e39590d94c114ebb81e78d4a1fccc38a 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -66,6 +66,11 @@ module IssuableCollections
     key = 'issuable_sort'
 
     cookies[key] = params[:sort] if params[:sort].present?
+
+    # id_desc and id_asc are old values for these two.
+    cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc'
+    cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc'
+
     params[:sort] = cookies[key]
   end
 
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 52682ef9dc9833eeb9d79665900137e4314ed4d6..c13333641d33e4da0a82de791828d40cd01b0b8d 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,5 @@
 module MembershipActions
   extend ActiveSupport::Concern
-  include MembersHelper
 
   def request_access
     membershipable.request_access(current_user)
@@ -10,28 +9,23 @@ module MembershipActions
   end
 
   def approve_access_request
-    @member = membershipable.requesters.find(params[:id])
-
-    return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
-
-    @member.accept_request
+    Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
 
     redirect_to polymorphic_url([membershipable, :members])
   end
 
   def leave
-    @member = membershipable.members.find_by(user_id: current_user) ||
-      membershipable.requesters.find_by(user_id: current_user)
-    Members::DestroyService.new(@member, current_user).execute
+    member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
+      execute(:all)
 
-    source_type = @member.real_source_type.humanize(capitalize: false)
+    source_type = membershipable.class.to_s.humanize(capitalize: false)
     notice =
-      if @member.request?
+      if member.request?
         "Your access request to the #{source_type} has been withdrawn."
       else
-        "You left the \"#{@member.source.human_name}\" #{source_type}."
+        "You left the \"#{membershipable.human_name}\" #{source_type}."
       end
-    redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+    redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
 
     redirect_to redirect_path, notice: notice
   end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index a69877edfd40b786925787593137e4a22f065921..c33d7eecb9f7941ba7f8d29d9436ee1abc652480 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -13,12 +13,12 @@ module ServiceParams
                     # `issue_events` and `merge_request_events` (singular!)
                     # See app/helpers/services_helper.rb for how we
                     # make those event names plural as special case.
-                    :issues_events, :merge_requests_events,
+                    :issues_events, :confidential_issues_events, :merge_requests_events,
                     :notify_only_broken_builds, :notify_only_broken_pipelines,
                     :add_pusher, :send_from_committer_email, :disable_diffs,
                     :external_wiki_url, :notify, :color,
                     :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
-                    :jira_issue_transition_id]
+                    :jira_issue_transition_id, :url, :project_key]
 
   # Parameters to ignore if no value is specified
   FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 29e243c66a33d134115c1c9f754985ca969aa720..99acd98ae1361f01b08bdcc11acde6db5ccfba33 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -7,7 +7,7 @@ module SpammableActions
 
   def mark_as_spam
     if SpamService.new(spammable).mark_as_spam!
-      redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+      redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
     else
       redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
     end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 036777c80c19f1edf6a29e19b4d53f91b6d6262b..3717c49f272f4d91f704267e5bc621e0c2adb79f 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -8,10 +8,16 @@ module ToggleAwardEmoji
   def toggle_award_emoji
     name = params.require(:name)
 
-    awardable.toggle_award_emoji(name, current_user)
-    TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+    if awardable.user_can_award?(current_user, name)
+      awardable.toggle_award_emoji(name, current_user)
 
-    render json: { ok: true }
+      todoable = to_todoable(awardable)
+      TodoService.new.new_award_emoji(todoable, current_user) if todoable
+
+      render json: { ok: true }
+    else
+      render json: { ok: false }
+    end
   end
 
   private
@@ -20,8 +26,10 @@ module ToggleAwardEmoji
     case awardable
     when Note
       awardable.noteable
-    else
+    when MergeRequest, Issue
       awardable
+    when Snippet
+      nil
     end
   end
 
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index 2a88350a4cabeb5d90ab1a87680caf3cec899409..d5031da867af6ac49fdebbe14b26b35507a259f9 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -1,9 +1,9 @@
 class Dashboard::LabelsController < Dashboard::ApplicationController
   def index
-    labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title)
+    labels = LabelsFinder.new(current_user).execute
 
     respond_to do |format|
-      format.json { render json: labels }
+      format.json { render json: labels.as_json(only: [:id, :title, :color]) }
     end
   end
 end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 1243bb96d4d45b3828cb602fee32d03f4d3e0ca3..d425d0f90146c918d428856ae977cabc5fdca5b6 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,11 +2,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
   before_action :find_todos, only: [:index, :destroy_all]
 
   def index
+    @sort = params[:sort]
     @todos = @todos.page(params[:page])
   end
 
   def destroy
-    TodoService.new.mark_todos_as_done([todo], current_user)
+    TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
 
     respond_to do |format|
       format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
@@ -27,10 +28,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
 
   private
 
-  def todo
-    @todo ||= find_todos.find(params[:id])
-  end
-
   def find_todos
     @todos ||= TodosFinder.new(current_user, params).execute
   end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 88a0c18180be3240e312a28513713d4bc8735c4e..a62c62113721c3d7ec49d129400f6b1367c3f4c3 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
   end
 
   def trending
-    @projects = TrendingProjectsFinder.new.execute(current_user)
-    @projects = filter_projects(@projects)
+    @projects = filter_projects(Project.trending)
     @projects = @projects.page(params[:page])
 
     respond_to do |format|
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a125364f74a8b0f7651fcb4cb995d01d02c..940a3ad20ba5495d26d759bf9664a0e0f1de568e 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -15,13 +15,22 @@ class Groups::GroupMembersController < Groups::ApplicationController
     end
 
     @members = @members.order('access_level DESC').page(params[:page]).per(50)
-    @requesters = @group.requesters if can?(current_user, :admin_group, @group)
+    @requesters = AccessRequestsFinder.new(@group).execute(current_user)
 
     @group_member = @group.group_members.new
   end
 
   def create
-    @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+    if params[:user_ids].blank?
+      return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
+    end
+
+    @group.add_users(
+      params[:user_ids].split(','),
+      params[:access_level],
+      current_user: current_user,
+      expires_at: params[:expires_at]
+    )
 
     redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
   end
@@ -35,10 +44,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
   end
 
   def destroy
-    @group_member = @group.members.find_by(id: params[:id]) ||
-      @group.requesters.find_by(id: params[:id])
-
-    Members::DestroyService.new(@group_member, current_user).execute
+    Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
 
     respond_to do |format|
       format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
@@ -63,7 +69,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
   protected
 
   def member_params
-    params.require(:group_member).permit(:access_level, :user_id)
+    params.require(:group_member).permit(:access_level, :user_id, :expires_at)
   end
 
   # MembershipActions concern
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29528b2cfaae4cb27cf0c05ddc4ad190691e3366
--- /dev/null
+++ b/app/controllers/groups/labels_controller.rb
@@ -0,0 +1,92 @@
+class Groups::LabelsController < Groups::ApplicationController
+  before_action :label, only: [:edit, :update, :destroy]
+  before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
+  before_action :save_previous_label_path, only: [:edit]
+
+  respond_to :html
+
+  def index
+    respond_to do |format|
+      format.html do
+        @labels = @group.labels.page(params[:page])
+      end
+
+      format.json do
+        available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
+        render json: available_labels.as_json(only: [:id, :title, :color])
+      end
+    end
+  end
+
+  def new
+    @label = @group.labels.new
+    @previous_labels_path = previous_labels_path
+  end
+
+  def create
+    @label = @group.labels.create(label_params)
+
+    if @label.valid?
+      redirect_to group_labels_path(@group)
+    else
+      render :new
+    end
+  end
+
+  def edit
+    @previous_labels_path = previous_labels_path
+  end
+
+  def update
+    if @label.update_attributes(label_params)
+      redirect_back_or_group_labels_path
+    else
+      render :edit
+    end
+  end
+
+  def destroy
+    @label.destroy
+
+    respond_to do |format|
+      format.html do
+        redirect_to group_labels_path(@group), notice: 'Label was removed'
+      end
+      format.js
+    end
+  end
+
+  protected
+
+  def authorize_admin_labels!
+    return render_404 unless can?(current_user, :admin_label, @group)
+  end
+
+  def authorize_read_labels!
+    return render_404 unless can?(current_user, :read_label, @group)
+  end
+
+  def label
+    @label ||= @group.labels.find(params[:id])
+  end
+
+  def label_params
+    params.require(:label).permit(:title, :description, :color)
+  end
+
+  def redirect_back_or_group_labels_path(options = {})
+    redirect_to previous_labels_path, options
+  end
+
+  def previous_labels_path
+    session.fetch(:previous_labels_path, fallback_path)
+  end
+
+  def fallback_path
+    group_labels_path(@group)
+  end
+
+  def save_previous_label_path
+    session[:previous_labels_path] = URI(request.referer || '').path
+  end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index cb82d62616c863835f5b77eac7851abef4054bf7..b83c3a872cf07bb9caaf583ae2fec7f83640fd94 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -121,7 +121,17 @@ class GroupsController < Groups::ApplicationController
   end
 
   def group_params
-    params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
+    params.require(:group).permit(
+      :avatar,
+      :description,
+      :lfs_enabled,
+      :name,
+      :path,
+      :public,
+      :request_access_enabled,
+      :share_with_group_lock,
+      :visibility_level
+    )
   end
 
   def load_events
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 7e8597a5eb3afe098348335198208b4834f709e4..256c41e6145efba3d42b7c3408917ef9be4f6d66 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,18 +1,17 @@
 class Import::BaseController < ApplicationController
   private
 
-  def get_or_create_namespace
+  def find_or_create_namespace(name, owner)
+    return current_user.namespace if name == owner
+    return current_user.namespace unless current_user.can_create_group?
+
     begin
-      namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user)
+      name = params[:target_namespace].presence || name
+      namespace = Group.create!(name: name, path: name, owner: current_user)
       namespace.add_owner(current_user)
+      namespace
     rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
-      namespace = Namespace.find_by_path_or_name(@target_namespace)
-      unless current_user.can?(:create_projects, namespace)
-        @already_been_taken = true
-        return false
-      end
+      Namespace.find_by_path_or_name(name)
     end
-
-    namespace
   end
 end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 944c73d139ae0be6dd26f0e0111f3c301c796ac7..6ea54744da83982edbd0633bb7e86632e6aa2a96 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -35,23 +35,20 @@ class Import::BitbucketController < Import::BaseController
   end
 
   def create
-    @repo_id = params[:repo_id] || ""
-    repo = client.project(@repo_id.gsub("___", "/"))
-    @project_name = repo["slug"]
-
-    repo_owner = repo["owner"]
-    repo_owner = current_user.username if repo_owner == client.user["user"]["username"]
-    @target_namespace = params[:new_namespace].presence || repo_owner
-
-    namespace = get_or_create_namespace || (render and return)
+    @repo_id = params[:repo_id].to_s
+    repo = client.project(@repo_id.gsub('___', '/'))
+    @project_name = repo['slug']
+    @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
 
     unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
-      @access_denied = true
-      render
-      return
+      render 'deploy_key' and return
     end
 
-    @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+    if current_user.can?(:create_projects, @target_namespace)
+      @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+    else
+      render 'unauthorized'
+    end
   end
 
   private
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 9c1b0eb20f43e005011e89889b41ca55c66844b1..ee7d498c59ce1cc0fe3b5e5e3d9908867abc81e0 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -40,15 +40,15 @@ class Import::GithubController < Import::BaseController
   def create
     @repo_id = params[:repo_id].to_i
     repo = client.repo(@repo_id)
-    @project_name = repo.name
-
-    repo_owner = repo.owner.login
-    repo_owner = current_user.username if repo_owner == client.user.login
-    @target_namespace = params[:new_namespace].presence || repo_owner
-
-    namespace = get_or_create_namespace || (render and return)
-
-    @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+    @project_name = params[:new_name].presence || repo.name
+    namespace_path = params[:target_namespace].presence || current_user.namespace_path
+    @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
+
+    if current_user.can?(:create_projects, @target_namespace)
+      @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
+    else
+      render 'unauthorized'
+    end
   end
 
   private
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 08130ee81764dc22fbcfb079cb1f848053853ea7..73837ffbe67711bf86bfa8daeb700253e4912dea 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -26,15 +26,14 @@ class Import::GitlabController < Import::BaseController
   def create
     @repo_id = params[:repo_id].to_i
     repo = client.project(@repo_id)
-    @project_name = repo["name"]
+    @project_name = repo['name']
+    @target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username'])
 
-    repo_owner = repo["namespace"]["path"]
-    repo_owner = current_user.username if repo_owner == client.user["username"]
-    @target_namespace = params[:new_namespace].presence || repo_owner
-
-    namespace = get_or_create_namespace || (render and return)
-
-    @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+    if current_user.can?(:create_projects, @target_namespace)
+      @project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+    else
+      render 'unauthorized'
+    end
   end
 
   private
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 7d0eff376354d84de066f7c9804e11a7ed8015ac..36d246d185bbe78289eaf94505dd80010508b85c 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,10 +1,9 @@
 class Import::GitlabProjectsController < Import::BaseController
   before_action :verify_gitlab_project_import_enabled
-  before_action :authenticate_admin!
 
   def new
-    @namespace_id = project_params[:namespace_id]
-    @namespace_name = Namespace.find(project_params[:namespace_id]).name
+    @namespace = Namespace.find(project_params[:namespace_id])
+    return render_404 unless current_user.can?(:create_projects, @namespace)
     @path = project_params[:path]
   end
 
@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController
       :path, :namespace_id, :file
     )
   end
-
-  def authenticate_admin!
-    render_404 unless current_user.is_admin?
-  end
 end
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
deleted file mode 100644
index a4c4ad230279d342b8e8ef43b135a4552b95c0a2..0000000000000000000000000000000000000000
--- a/app/controllers/import/gitorious_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class Import::GitoriousController < Import::BaseController
-  before_action :verify_gitorious_import_enabled
-
-  def new
-    redirect_to client.authorize_url(callback_import_gitorious_url)
-  end
-
-  def callback
-    session[:gitorious_repos] = params[:repos]
-    redirect_to status_import_gitorious_path
-  end
-
-  def status
-    @repos = client.repos
-
-    @already_added_projects = current_user.created_projects.where(import_type: "gitorious")
-    already_added_projects_names = @already_added_projects.pluck(:import_source)
-
-    @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
-  end
-
-  def jobs
-    jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status])
-    render json: jobs
-  end
-
-  def create
-    @repo_id = params[:repo_id]
-    repo = client.repo(@repo_id)
-    @target_namespace = params[:new_namespace].presence || repo.namespace
-    @project_name = repo.name
-
-    namespace = get_or_create_namespace || (render and return)
-
-    @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute
-  end
-
-  private
-
-  def client
-    @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos])
-  end
-
-  def verify_gitorious_import_enabled
-    render_404 unless gitorious_import_enabled?
-  end
-end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 014b9b43ff26f955f969a883defd154817555721..c736200a1040d4f3cadf0c6e9b6576cd8f25bdf3 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,7 +11,8 @@ class JwtController < ApplicationController
     service = SERVICES[params[:service]]
     return head :not_found unless service
 
-    result = service.new(@project, @user, auth_params).execute
+    result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
+      execute(authentication_abilities: @authentication_result.authentication_abilities)
 
     render json: result, status: result[:http_status]
   end
@@ -19,31 +20,37 @@ class JwtController < ApplicationController
   private
 
   def authenticate_project_or_user
-    authenticate_with_http_basic do |login, password|
-      # if it's possible we first try to authenticate project with login and password
-      @project = authenticate_project(login, password)
-      return if @project
+    @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
 
-      @user = authenticate_user(login, password)
-      return if @user
+    authenticate_with_http_basic do |login, password|
+      @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
 
-      render_403
+      render_unauthorized unless @authentication_result.success? &&
+        (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
     end
+  rescue Gitlab::Auth::MissingPersonalTokenError
+    render_missing_personal_token
   end
 
-  def auth_params
-    params.permit(:service, :scope, :account, :client_id)
+  def render_missing_personal_token
+    render json: {
+      errors: [
+        { code: 'UNAUTHORIZED',
+          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
   end
 
-  def authenticate_project(login, password)
-    if login == 'gitlab-ci-token'
-      Project.find_by(builds_enabled: true, runners_token: password)
-    end
+  def render_unauthorized
+    render json: {
+      errors: [
+        { code: 'UNAUTHORIZED',
+          message: 'HTTP Basic: Access denied' }
+      ] }, status: 401
   end
 
-  def authenticate_user(login, password)
-    user = Gitlab::Auth.find_with_user_password(login, password)
-    Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
-    user
+  def auth_params
+    params.permit(:service, :scope, :account, :client_id)
   end
 end
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f3759b4c0ea2b77696e0342d5186b703288fc455
--- /dev/null
+++ b/app/controllers/koding_controller.rb
@@ -0,0 +1,15 @@
+class KodingController < ApplicationController
+  before_action :check_integration!, :authenticate_user!, :reject_blocked!
+  layout 'koding'
+
+  def index
+    path = File.join(Rails.root, 'doc/user/project/koding.md')
+    @markdown = File.read(path)
+  end
+
+  private
+
+  def check_integration!
+    render_404 unless current_application_settings.koding_enabled?
+  end
+end
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
deleted file mode 100644
index 5a94dcb0dbda623f540ddf128c0e1d25c9829bf9..0000000000000000000000000000000000000000
--- a/app/controllers/namespaces_controller.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class NamespacesController < ApplicationController
-  skip_before_action :authenticate_user!
-
-  def show
-    namespace = Namespace.find_by(path: params[:id])
-
-    if namespace
-      if namespace.is_a?(Group)
-        group = namespace
-      else
-        user = namespace.owner
-      end
-    end
-
-    if user
-      redirect_to user_path(user)
-    elsif group && can?(current_user, :read_group, namespace)
-      redirect_to group_path(group)
-    elsif current_user.nil?
-      authenticate_user!
-    else
-      render_404
-    end
-  end
-end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e37e9e136dbb363cf18a60fc41937249072cdf7f..9eb75bb389185e5e4442882681ca46749434151f 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
   # A U2F (universal 2nd factor) device's information is stored after successful
   # registration, which is then used while 2FA authentication is taking place.
   def create_u2f
-    @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
+    @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
 
     if @u2f_registration.persisted?
       session.delete(:challenges)
-      redirect_to profile_account_path, notice: "Your U2F device was registered!"
+      redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
     else
       @qr_code = build_qr_code
       setup_u2f_registration
@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
   # Actual communication is performed using a Javascript API
   def setup_u2f_registration
     @u2f_registration ||= U2fRegistration.new
-    @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
+    @u2f_registrations = current_user.u2f_registrations
     u2f = U2F::U2F.new(u2f_app_id)
 
     registration_requests = u2f.registration_requests
-    sign_requests = u2f.authentication_requests(@registration_key_handles)
+    sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
     session[:challenges] = registration_requests.map(&:challenge)
 
     gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
                     register_requests: registration_requests,
                     sign_requests: sign_requests })
   end
+
+  def u2f_registration_params
+    params.require(:u2f_registration).permit(:device_response, :name)
+  end
 end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c02fe85c3cc2d7c791e8536f684dadd565f0e0b9
--- /dev/null
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -0,0 +1,7 @@
+class Profiles::U2fRegistrationsController < Profiles::ApplicationController
+  def destroy
+    u2f_registration = current_user.u2f_registrations.find(params[:id])
+    u2f_registration.destroy
+    redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
+  end
+end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index c5fa756d02bb8098603cb9714395a1e7eccf1cc7..f0c71725ea8c851a66c38bc209472f9657f8fa39 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -26,7 +26,15 @@ class ProfilesController < Profiles::ApplicationController
 
   def reset_private_token
     if current_user.reset_authentication_token!
-      flash[:notice] = "Token was successfully updated"
+      flash[:notice] = "Private token was successfully reset"
+    end
+
+    redirect_to profile_account_path
+  end
+
+  def reset_incoming_email_token
+    if current_user.reset_incoming_email_token!
+      flash[:notice] = "Incoming email token was successfully reset"
     end
 
     redirect_to profile_account_path
@@ -73,7 +81,8 @@ class ProfilesController < Profiles::ApplicationController
       :skype,
       :twitter,
       :username,
-      :website_url
+      :website_url,
+      :organization
     )
   end
 end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 996909a28c6805156e19f26705843fad413b39a1..b2ff36f65380395359a788b04c66d0f468ee4e1b 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -83,10 +83,11 @@ class Projects::ApplicationController < ApplicationController
   end
 
   def apply_diff_view_cookie!
+    @show_changes_tab = params[:view].present?
     cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
   end
 
   def builds_enabled
-    return render_404 unless @project.builds_enabled?
+    return render_404 unless @project.feature_available?(:builds, current_user)
   end
 end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 7241949393b5dfc18024e62d78d1aa6b622a0fdb..5922263796145f46ce6330ae68d540e8a55ddd29 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,22 +1,25 @@
 class Projects::ArtifactsController < Projects::ApplicationController
+  include ExtractsPath
+
   layout 'project'
   before_action :authorize_read_build!
   before_action :authorize_update_build!, only: [:keep]
+  before_action :extract_ref_name_and_path
   before_action :validate_artifacts!
 
   def download
-    unless artifacts_file.file_storage?
-      return redirect_to artifacts_file.url
+    if artifacts_file.file_storage?
+      send_file artifacts_file.path, disposition: 'attachment'
+    else
+      redirect_to artifacts_file.url
     end
-
-    send_file artifacts_file.path, disposition: 'attachment'
   end
 
   def browse
     directory = params[:path] ? "#{params[:path]}/" : ''
     @entry = build.artifacts_metadata_entry(directory)
 
-    return render_404 unless @entry.exists?
+    render_404 unless @entry.exists?
   end
 
   def file
@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
     redirect_to namespace_project_build_path(project.namespace, project, build)
   end
 
+  def latest_succeeded
+    target_path = artifacts_action_path(@path, project, build)
+
+    if target_path
+      redirect_to(target_path)
+    else
+      render_404
+    end
+  end
+
   private
 
+  def extract_ref_name_and_path
+    return unless params[:ref_name_and_path]
+
+    @ref_name, @path = extract_ref(params[:ref_name_and_path])
+  end
+
   def validate_artifacts!
-    render_404 unless build.artifacts?
+    render_404 unless build && build.artifacts?
   end
 
   def build
-    @build ||= project.builds.find_by!(id: params[:build_id])
+    @build ||= build_from_id || build_from_ref
+  end
+
+  def build_from_id
+    project.builds.find_by(id: params[:build_id]) if params[:build_id]
+  end
+
+  def build_from_ref
+    return unless @ref_name
+
+    builds = project.latest_successful_builds_for(@ref_name)
+    builds.find_by(name: params[:job])
   end
 
   def artifacts_file
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 5962f74c39bfa431debf2732d265efd00fee2e4d..ada7db3c552bf6c18dbd53b9e94cd8452abf5ecc 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -4,7 +4,7 @@ class Projects::AvatarsController < Projects::ApplicationController
   before_action :authorize_admin_project!, only: [:destroy]
 
   def show
-    @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
+    @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
     if @blob
       headers['X-Content-Type-Options'] = 'nosniff'
 
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index cdf9a04bacfcfb80d50e15bf590508db4e1c2988..b78cc6585ba1b250f3e851e1e198526c663ff086 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -38,12 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
   end
 
   def update
-    if params[:file_path].present?
-      @previous_path = @path
-      @path = params[:file_path]
-      @commit_params[:file_path] = @path
-    end
-
+    @path = params[:file_path] if params[:file_path].present?
     after_edit_path =
       if from_merge_request && @target_branch == @ref
         diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
@@ -143,6 +138,8 @@ class Projects::BlobController < Projects::ApplicationController
           params[:file_name] = params[:file].original_filename
         end
         File.join(@path, params[:file_name])
+      elsif params[:file_path].present?
+        params[:file_path]
       else
         @path
       end
@@ -155,6 +152,7 @@ class Projects::BlobController < Projects::ApplicationController
     @commit_params = {
       file_path: @file_path,
       commit_message: params[:commit_message],
+      previous_path: @path,
       file_content: params[:content],
       file_content_encoding: params[:encoding],
       last_commit_sha: params[:last_commit_sha]
diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dad38fff6b9854d41e264c79e4e4c6471481d214
--- /dev/null
+++ b/app/controllers/projects/boards/application_controller.rb
@@ -0,0 +1,15 @@
+module Projects
+  module Boards
+    class ApplicationController < Projects::ApplicationController
+      respond_to :json
+
+      rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+      private
+
+      def record_not_found(exception)
+        render json: { error: exception.message }, status: :not_found
+      end
+    end
+  end
+end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc33e1405f2e99b73315d2334d1ec5f346557126
--- /dev/null
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -0,0 +1,86 @@
+module Projects
+  module Boards
+    class IssuesController < Boards::ApplicationController
+      before_action :authorize_read_issue!, only: [:index]
+      before_action :authorize_create_issue!, only: [:create]
+      before_action :authorize_update_issue!, only: [:update]
+
+      def index
+        issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
+        issues = issues.page(params[:page])
+
+        render json: {
+          issues: serialize_as_json(issues),
+          size: issues.total_count
+        }
+      end
+
+      def create
+        service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
+        issue = service.execute
+
+        if issue.valid?
+          render json: serialize_as_json(issue)
+        else
+          render json: issue.errors, status: :unprocessable_entity
+        end
+      end
+
+      def update
+        service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
+
+        if service.execute(issue)
+          head :ok
+        else
+          head :unprocessable_entity
+        end
+      end
+
+      private
+
+      def issue
+        @issue ||=
+          IssuesFinder.new(current_user, project_id: project.id)
+                      .execute
+                      .where(iid: params[:id])
+                      .first!
+      end
+
+      def authorize_read_issue!
+        return render_403 unless can?(current_user, :read_issue, project)
+      end
+
+      def authorize_create_issue!
+        return render_403 unless can?(current_user, :admin_issue, project)
+      end
+
+      def authorize_update_issue!
+        return render_403 unless can?(current_user, :update_issue, issue)
+      end
+
+      def filter_params
+        params.merge(board_id: params[:board_id], id: params[:list_id])
+      end
+
+      def move_params
+        params.permit(:board_id, :id, :from_list_id, :to_list_id)
+      end
+
+      def issue_params
+        params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
+      end
+
+      def serialize_as_json(resource)
+        resource.as_json(
+          labels: true,
+          only: [:iid, :title, :confidential, :due_date],
+          include: {
+            assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+            milestone: { only: [:id, :title] }
+          },
+          user: current_user
+        )
+      end
+    end
+  end
+end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..67e3c9add81ea3078d6175774130aebecb397ac9
--- /dev/null
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -0,0 +1,84 @@
+module Projects
+  module Boards
+    class ListsController < Boards::ApplicationController
+      before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
+      before_action :authorize_read_list!, only: [:index]
+
+      def index
+        render json: serialize_as_json(board.lists)
+      end
+
+      def create
+        list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
+
+        if list.valid?
+          render json: serialize_as_json(list)
+        else
+          render json: list.errors, status: :unprocessable_entity
+        end
+      end
+
+      def update
+        list = board.lists.movable.find(params[:id])
+        service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
+
+        if service.execute(list)
+          head :ok
+        else
+          head :unprocessable_entity
+        end
+      end
+
+      def destroy
+        list = board.lists.destroyable.find(params[:id])
+        service = ::Boards::Lists::DestroyService.new(project, current_user)
+
+        if service.execute(list)
+          head :ok
+        else
+          head :unprocessable_entity
+        end
+      end
+
+      def generate
+        service = ::Boards::Lists::GenerateService.new(project, current_user)
+
+        if service.execute(board)
+          render json: serialize_as_json(board.lists.movable)
+        else
+          head :unprocessable_entity
+        end
+      end
+
+      private
+
+      def authorize_admin_list!
+        return render_403 unless can?(current_user, :admin_list, project)
+      end
+
+      def authorize_read_list!
+        return render_403 unless can?(current_user, :read_list, project)
+      end
+
+      def board
+        @board ||= project.boards.find(params[:board_id])
+      end
+
+      def list_params
+        params.require(:list).permit(:label_id)
+      end
+
+      def move_params
+        params.require(:list).permit(:position)
+      end
+
+      def serialize_as_json(resource)
+        resource.as_json(
+          only: [:id, :list_type, :position],
+          methods: [:title],
+          label: true
+        )
+      end
+    end
+  end
+end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..808affa4f9843027e17bcc0d332e1786cf096a56
--- /dev/null
+++ b/app/controllers/projects/boards_controller.rb
@@ -0,0 +1,37 @@
+class Projects::BoardsController < Projects::ApplicationController
+  include IssuableCollections
+
+  before_action :authorize_read_board!, only: [:index, :show]
+
+  def index
+    @boards = ::Boards::ListService.new(project, current_user).execute
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: serialize_as_json(@boards)
+      end
+    end
+  end
+
+  def show
+    @board = project.boards.find(params[:id])
+
+    respond_to do |format|
+      format.html
+      format.json do
+        render json: serialize_as_json(@board)
+      end
+    end
+  end
+
+  private
+
+  def authorize_read_board!
+    return access_denied! unless can?(current_user, :read_board, project)
+  end
+
+  def serialize_as_json(resource)
+    resource.as_json(only: [:id])
+  end
+end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 48fe81b0d7456b66f0e200447dea7fa9f34ed0b3..2de8ada3e29604cf787c3d879697ffeae2cd66d0 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController
       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.json do
+        render json: @repository.branch_names
+      end
+    end
   end
 
   def recent
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 12195c3cbb82704fd93d8648909551a1a75e5f19..fbe391fc58cd949ac6104c12401797a74d0b1e63 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
     respond_to do |format|
       format.html
       format.json do
-        render json: @build.to_json(methods: :trace_html)
+        render json: {
+          id: @build.id,
+          status: @build.status,
+          trace_html: @build.trace_html
+        }
       end
     end
   end
@@ -43,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
   def trace
     respond_to do |format|
       format.json do
-        render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
+        state = params[:state].presence
+        render json: @build.trace_with_state(state: state).
+          merge!(id: @build.id, status: @build.status)
       end
     end
   end
@@ -74,12 +80,12 @@ class Projects::BuildsController < Projects::ApplicationController
   def erase
     @build.erase(erased_by: current_user)
     redirect_to namespace_project_build_path(project.namespace, project, @build),
-                notice: "Build has been sucessfully erased!"
+                notice: "Build has been successfully erased!"
   end
 
   def raw
-    if @build.has_trace?
-      send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+    if @build.has_trace_file?
+      send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline'
     else
       render_404
     end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index f44e9bb3fd7d1c3324c549c6126a5b1ae013bfe5..cdfc1ba7b9292603e6bef8e4cfb753972f54c143 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -10,10 +10,11 @@ class Projects::CommitController < Projects::ApplicationController
   before_action :require_non_empty_project
   before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
   before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+  before_action :authorize_read_pipeline!, only: [:pipelines]
   before_action :authorize_read_commit_status!, only: [:builds]
   before_action :commit
-  before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
-  before_action :define_status_vars, only: [:show, :builds]
+  before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
+  before_action :define_status_vars, only: [:show, :builds, :pipelines]
   before_action :define_note_vars, only: [:show, :diff_for_path]
   before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
 
@@ -31,6 +32,9 @@ class Projects::CommitController < Projects::ApplicationController
     render_diff_for_path(@commit.diffs(diff_options))
   end
 
+  def pipelines
+  end
+
   def builds
   end
 
@@ -93,11 +97,7 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def commit
-    @commit ||= @project.commit(params[:id])
-  end
-
-  def pipelines
-    @pipelines ||= project.pipelines.where(sha: commit.sha)
+    @noteable = @commit ||= @project.commit(params[:id])
   end
 
   def ci_builds
@@ -134,8 +134,9 @@ class Projects::CommitController < Projects::ApplicationController
   end
 
   def define_status_vars
-    @statuses = CommitStatus.where(pipeline: pipelines).relevant
-    @builds = Ci::Build.where(pipeline: pipelines).relevant
+    @ci_pipelines = project.pipelines.where(sha: commit.sha)
+    @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
+    @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
   end
 
   def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index a52c614b259accce46bd0b82f36b0b4e4d3d8080..aba87b6144b0a7614c69216193f24738670f198a 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController
 
     @commits =
       if search.present?
-        @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
+        @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
       else
         @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
       end
@@ -26,8 +26,15 @@ class Projects::CommitsController < Projects::ApplicationController
 
     respond_to do |format|
       format.html
-      format.json { pager_json("projects/commits/_commits", @commits.size) }
       format.atom { render layout: false }
+
+      format.json do
+        pager_json(
+          'projects/commits/_commits',
+          @commits.size,
+          project: @project,
+          ref: @ref)
+      end
     end
   end
 end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16a7b1fc6e26618ba634279476c9bfdf03012ba2
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -0,0 +1,67 @@
+class Projects::CycleAnalyticsController < Projects::ApplicationController
+  include ActionView::Helpers::DateHelper
+  include ActionView::Helpers::TextHelper
+
+  before_action :authorize_read_cycle_analytics!
+
+  def show
+    @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+
+    respond_to do |format|
+      format.html
+      format.json { render json: cycle_analytics_json }
+    end
+  end
+
+  private
+
+  def parse_start_date
+    case cycle_analytics_params[:start_date]
+    when '30' then 30.days.ago
+    when '90' then 90.days.ago
+    else 90.days.ago
+    end
+  end
+
+  def cycle_analytics_params
+    return {} unless params[:cycle_analytics].present?
+
+    { start_date: params[:cycle_analytics][:start_date] }
+  end
+
+  def cycle_analytics_json
+    cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
+                                 [:plan, "Plan", "Time before an issue starts implementation"],
+                                 [:code, "Code", "Time until first merge request"],
+                                 [:test, "Test", "Total test time for all commits/merges"],
+                                 [:review, "Review", "Time between merge request creation and merge/close"],
+                                 [:staging, "Staging", "From merge request merge until deploy to production"],
+                                 [:production, "Production", "From issue creation until deploy to production"]]
+
+    stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+      value = @cycle_analytics.send(stage_method).presence
+
+      stats << {
+        title: stage_text,
+        description: stage_description,
+        value: value && !value.zero? ? distance_of_time_in_words(value) : nil
+      }
+      stats
+    end
+
+    issues = @cycle_analytics.summary.new_issues
+    commits = @cycle_analytics.summary.commits
+    deploys = @cycle_analytics.summary.deploys
+
+    summary = [
+      { title: "New Issue".pluralize(issues), value: issues },
+      { title: "Commit".pluralize(commits), value: commits },
+      { title: "Deploy".pluralize(deploys), value: deploys }
+    ]
+
+    {
+      summary: summary,
+      stats: stats
+    }
+  end
+end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d174e1145a783a6026ef1295b9e706046f8f2c9f
--- /dev/null
+++ b/app/controllers/projects/discussions_controller.rb
@@ -0,0 +1,43 @@
+class Projects::DiscussionsController < Projects::ApplicationController
+  before_action :module_enabled
+  before_action :merge_request
+  before_action :discussion
+  before_action :authorize_resolve_discussion!
+
+  def resolve
+    discussion.resolve!(current_user)
+
+    MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+
+    render json: {
+      resolved_by: discussion.resolved_by.try(:name),
+      discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
+    }
+  end
+
+  def unresolve
+    discussion.unresolve!
+
+    render json: {
+      discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
+    }
+  end
+
+  private
+
+  def merge_request
+    @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id])
+  end
+
+  def discussion
+    @discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
+  end
+
+  def authorize_resolve_discussion!
+    access_denied! unless discussion.can_resolve?(current_user)
+  end
+
+  def module_enabled
+    render_404 unless @project.feature_available?(:merge_requests, current_user)
+  end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 58678f96879b160a8cb6d5dac34d453ac648a1bf..ea22b2dcc15a5e7aff6c5822141af82e8f1e6d6e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
   layout 'project'
   before_action :authorize_read_environment!
   before_action :authorize_create_environment!, only: [:new, :create]
-  before_action :authorize_update_environment!, only: [:edit, :update, :destroy]
-  before_action :environment, only: [:show, :edit, :update, :destroy]
+  before_action :authorize_create_deployment!, only: [:stop]
+  before_action :authorize_update_environment!, only: [:edit, :update]
+  before_action :environment, only: [:show, :edit, :update, :stop]
 
   def index
-    @environments = project.environments
+    @scope = params[:scope]
+    @all_environments = project.environments
+    @environments =
+      if @scope == 'stopped'
+        @all_environments.stopped
+      else
+        @all_environments.available
+      end
   end
 
   def show
@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
     end
   end
 
-  def destroy
-    if @environment.destroy
-      flash[:notice] = 'Environment was successfully removed.'
-    else
-      flash[:alert] = 'Failed to remove environment.'
-    end
+  def stop
+    return render_404 unless @environment.stoppable?
 
-    redirect_to namespace_project_environments_path(project.namespace, project)
+    new_action = @environment.stop!(current_user)
+    redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
   end
 
   private
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7c21bd181dc04d579ac8798e6cbee14d49380ef2..3f41916e6d3a38a8c37d2f3766cb21b90b33b844 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
   include ActionController::HttpAuthentication::Basic
   include KerberosSpnegoHelper
 
-  attr_reader :user
+  attr_reader :authentication_result
+
+  delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
+
+  alias_method :user, :actor
 
   # Git clients will not know what authenticity token to send along
   skip_before_action :verify_authenticity_token
@@ -15,36 +19,34 @@ class Projects::GitHttpClientController < Projects::ApplicationController
   private
 
   def authenticate_user
-    if project && project.public? && download_request?
-      return # Allow access
-    end
+    @authentication_result = Gitlab::Auth::Result.new
 
     if allow_basic_auth? && basic_auth_provided?
       login, password = user_name_and_password(request)
-      auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
-      if auth_result.type == :ci && download_request?
-        @ci = true
-      elsif auth_result.type == :oauth && !download_request?
-        # Not allowed
-      else
-        @user = auth_result.user
-      end
 
-      if ci? || user
+      if handle_basic_authentication(login, password)
         return # Allow access
       end
     elsif allow_kerberos_spnego_auth? && spnego_provided?
-      @user = find_kerberos_user
+      kerberos_user = find_kerberos_user
+
+      if kerberos_user
+        @authentication_result = Gitlab::Auth::Result.new(
+          kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
 
-      if user
         send_final_spnego_response
         return # Allow access
       end
+    elsif project && download_request? && Guest.can?(:download_code, project)
+      @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
+
+      return # Allow access
     end
 
     send_challenges
     render plain: "HTTP Basic: Access denied\n", status: 401
+  rescue Gitlab::Auth::MissingPersonalTokenError
+    render_missing_personal_token
   end
 
   def basic_auth_provided?
@@ -91,6 +93,13 @@ class Projects::GitHttpClientController < Projects::ApplicationController
     [nil, nil]
   end
 
+  def render_missing_personal_token
+    render plain: "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
+  end
+
   def repository
     _, suffix = project_id_with_suffix
     if suffix == '.wiki.git'
@@ -104,7 +113,44 @@ class Projects::GitHttpClientController < Projects::ApplicationController
     render plain: 'Not Found', status: :not_found
   end
 
+  def handle_basic_authentication(login, password)
+    @authentication_result = Gitlab::Auth.find_for_git_client(
+      login, password, project: project, ip: request.ip)
+
+    return false unless @authentication_result.success?
+
+    if download_request?
+      authentication_has_download_access?
+    else
+      authentication_has_upload_access?
+    end
+  end
+
   def ci?
-    @ci.present?
+    authentication_result.ci?(project)
+  end
+
+  def lfs_deploy_token?
+    authentication_result.lfs_deploy_token?(project)
+  end
+
+  def authentication_has_download_access?
+    has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
+  end
+
+  def authentication_has_upload_access?
+    has_authentication_ability?(:push_code)
+  end
+
+  def has_authentication_ability?(capability)
+    (authentication_abilities || []).include?(capability)
+  end
+
+  def authentication_project
+    authentication_result.project
+  end
+
+  def verify_workhorse_api!
+    Gitlab::Workhorse.verify_api_request!(request.headers)
   end
 end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index b4373ef89efa6575ddadb01311c627d3762346e8..13caeb42d4061eb74dc1c76b3e887ef388eacb1d 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,6 +1,8 @@
 # This file should be identical in GitLab Community Edition and Enterprise Edition
 
 class Projects::GitHttpController < Projects::GitHttpClientController
+  before_action :verify_workhorse_api!
+
   # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
   # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
   def info_refs
@@ -56,6 +58,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
   end
 
   def render_ok
+    set_workhorse_internal_api_content_type
     render json: Gitlab::Workhorse.git_http_ok(repository, user)
   end
 
@@ -75,15 +78,11 @@ class Projects::GitHttpController < Projects::GitHttpClientController
   def upload_pack_allowed?
     return false unless Gitlab.config.gitlab_shell.upload_pack
 
-    if user
-      access_check.allowed?
-    else
-      ci? || project.public?
-    end
+    access_check.allowed? || ci?
   end
 
   def access
-    @access ||= Gitlab::GitAccess.new(user, project, 'http')
+    @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
   end
 
   def access_check
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 092ef32e6e397d02a7bca135ecd4290c7494dbb3..923e7340e6925163fb845e4542c3a163dfb0bbbc 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController
 
     @languages = @languages.map do |language|
       name, share = language
-      color = Digest::SHA256.hexdigest(name)[0...6]
+      color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}"
       {
         value: (share.to_f * 100 / total).round(2),
         label: name,
-        color: "##{color}",
-        highlight: "##{color}"
+        color: color,
+        highlight: color
       }
     end
 
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa85322b5486c8aa930fdf34c459125c3c..9eaf26a0dbf966743dcfe1998824b94771491313 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -1,25 +1,53 @@
 class Projects::GroupLinksController < Projects::ApplicationController
   layout 'project_settings'
   before_action :authorize_admin_project!
+  before_action :authorize_admin_project_member!, only: [:update]
 
   def index
     @group_links = project.project_group_links.all
+
+    @skip_groups = @group_links.pluck(:group_id)
+    @skip_groups << project.namespace_id unless project.personal?
   end
 
   def create
-    group = Group.find(params[:link_group_id])
-    return render_404 unless can?(current_user, :read_group, group)
+    group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
+
+    if group
+      return render_404 unless can?(current_user, :read_group, group)
 
-    project.project_group_links.create(
-      group: group, group_access: params[:link_group_access]
-    )
+      project.project_group_links.create(
+        group: group,
+        group_access: params[:link_group_access],
+        expires_at: params[:expires_at]
+      )
+    else
+      flash[:alert] = 'Please select a group.'
+    end
 
     redirect_to namespace_project_group_links_path(project.namespace, project)
   end
 
+  def update
+    @group_link = @project.project_group_links.find(params[:id])
+
+    @group_link.update_attributes(group_link_params)
+  end
+
   def destroy
     project.project_group_links.find(params[:id]).destroy
 
-    redirect_to namespace_project_group_links_path(project.namespace, project)
+    respond_to do |format|
+      format.html do
+        redirect_to namespace_project_group_links_path(project.namespace, project)
+      end
+      format.js { head :ok }
+    end
+  end
+
+  protected
+
+  def group_link_params
+    params.require(:group_link).permit(:group_access, :expires_at)
   end
 end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b56240463879485eaeccfd8e040f7052c882e9c5..0ae8ff98009ab8f58593021d1e6275f254cb9166 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -59,6 +59,7 @@ class Projects::HooksController < Projects::ApplicationController
       :pipeline_events,
       :enable_ssl_verification,
       :issues_events,
+      :confidential_issues_events,
       :merge_requests_events,
       :note_events,
       :push_events,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e9fb11e8f94860a866f05193fb8427c8dba4a433..3f1a1d1c51167c0181a0f099f329b909bb37bd02 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,26 +20,16 @@ class Projects::IssuesController < Projects::ApplicationController
   # Allow modify issue
   before_action :authorize_update_issue!, only: [:edit, :update]
 
-  # Allow issues bulk update
-  before_action :authorize_admin_issues!, only: [:bulk_update]
-
   respond_to :html
 
   def index
-    terms = params['issue_search']
     @issues = issues_collection
+    @issues = @issues.page(params[:page])
 
-    if terms.present?
-      if terms =~ /\A#(\d+)\z/
-        @issues = @issues.where(iid: $1)
-      else
-        @issues = @issues.full_search(terms)
-      end
+    if params[:label_name].present?
+      @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
     end
 
-    @issues = @issues.page(params[:page])
-    @labels = @project.labels.where(title: params[:label_name])
-
     respond_to do |format|
       format.html
       format.atom { render layout: false }
@@ -66,7 +56,7 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def show
-    raw_notes = @issue.notes_with_associations.fresh
+    raw_notes = @issue.notes.inc_relations_for_view.fresh
 
     @notes = Banzai::NoteRenderer.
       render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@@ -122,9 +112,13 @@ class Projects::IssuesController < Projects::ApplicationController
       end
 
       format.json do
-        render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
+        render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
       end
     end
+
+  rescue ActiveRecord::StaleObjectError
+    @conflict = true
+    render :edit
   end
 
   def referenced_merge_requests
@@ -164,24 +158,11 @@ class Projects::IssuesController < Projects::ApplicationController
     end
   end
 
-  def bulk_update
-    result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
-
-    respond_to do |format|
-      format.json do
-        render json: { notice: "#{result[:count]} issues updated" }
-      end
-    end
-  end
-
   protected
 
   def issue
-    @issue ||= begin
-                 @project.issues.find_by!(iid: params[:id])
-               rescue ActiveRecord::RecordNotFound
-                 redirect_old
-               end
+    # The Sortable default scope causes performance issues when used with find_by
+    @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
   end
   alias_method :subscribable_resource, :issue
   alias_method :issuable, :issue
@@ -201,7 +182,7 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def module_enabled
-    return render_404 unless @project.issues_enabled && @project.default_issues_tracker?
+    return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
   end
 
   def redirect_to_external_issue_tracker
@@ -212,7 +193,7 @@ class Projects::IssuesController < Projects::ApplicationController
     if action_name == 'new'
       redirect_to external.new_issue_path
     else
-      redirect_to external.issues_url
+      redirect_to external.project_path
     end
   end
 
@@ -226,7 +207,6 @@ class Projects::IssuesController < Projects::ApplicationController
 
     if issue
       redirect_to issue_path(issue)
-      return
     else
       raise ActiveRecord::RecordNotFound.new
     end
@@ -235,20 +215,7 @@ class Projects::IssuesController < Projects::ApplicationController
   def issue_params
     params.require(:issue).permit(
       :title, :assignee_id, :position, :description, :confidential,
-      :milestone_id, :due_date, :state_event, :task_num, label_ids: []
-    )
-  end
-
-  def bulk_update_params
-    params.require(:update).permit(
-      :issues_ids,
-      :assignee_id,
-      :milestone_id,
-      :state_event,
-      :subscription_event,
-      label_ids: [],
-      add_label_ids: [],
-      remove_label_ids: []
+      :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
     )
   end
 end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 0ca675623e55d42ee72800f16a46bc3ba569db50..42fd09e9b7e0250d96e202b51ec9d22f2987f720 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
 
   before_action :module_enabled
   before_action :label, only: [:edit, :update, :destroy]
+  before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
   before_action :authorize_read_label!
-  before_action :authorize_admin_labels!, only: [
-    :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
-  ]
+  before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
+                                                 :generate, :destroy, :remove_priority,
+                                                 :set_priorities]
 
   respond_to :js, :html
 
   def index
-    @labels = @project.labels.unprioritized.page(params[:page])
-    @prioritized_labels = @project.labels.prioritized
+    @prioritized_labels = @available_labels.prioritized(@project)
+    @labels = @available_labels.unprioritized(@project).page(params[:page])
 
     respond_to do |format|
       format.html
       format.json do
-        render json: @project.labels
+        render json: @available_labels.as_json(only: [:id, :title, :color])
       end
     end
   end
@@ -30,9 +31,15 @@ class Projects::LabelsController < Projects::ApplicationController
     @label = @project.labels.create(label_params)
 
     if @label.valid?
-      redirect_to namespace_project_labels_path(@project.namespace, @project)
+      respond_to do |format|
+        format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
+        format.json { render json: @label }
+      end
     else
-      render 'new'
+      respond_to do |format|
+        format.html { render :new }
+        format.json { render json: { message: @label.errors.messages }, status: 400 }
+      end
     end
   end
 
@@ -43,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
     if @label.update_attributes(label_params)
       redirect_to namespace_project_labels_path(@project.namespace, @project)
     else
-      render 'edit'
+      render :edit
     end
   end
 
@@ -62,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
 
   def destroy
     @label.destroy
+    @labels = find_labels
 
     respond_to do |format|
       format.html do
@@ -74,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
 
   def remove_priority
     respond_to do |format|
-      if label.update_attribute(:priority, nil)
+      label = @available_labels.find(params[:id])
+
+      if label.unprioritize!(project)
         format.json { render json: label }
       else
-        message = label.errors.full_messages.uniq.join('. ')
-        format.json { render json: { message: message }, status: :unprocessable_entity }
+        format.json { head :unprocessable_entity }
       end
     end
   end
 
   def set_priorities
     Label.transaction do
-      params[:label_ids].each_with_index do |label_id, index|
-        label = @project.labels.find_by_id(label_id)
-        label.update_attribute(:priority, index) if label
+      available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
+      label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
+
+      label_ids.each_with_index do |label_id, index|
+        label = @available_labels.find(label_id)
+        label.prioritize!(project, index)
       end
     end
 
@@ -99,7 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController
   protected
 
   def module_enabled
-    unless @project.issues_enabled || @project.merge_requests_enabled
+    unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
       return render_404
     end
   end
@@ -113,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
   end
   alias_method :subscribable_resource, :label
 
+  def find_labels
+    @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+  end
+
   def authorize_admin_labels!
     return render_404 unless can?(current_user, :admin_label, @project)
   end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 69066cb40e671286810267fb4650699bf66aea58..9005b104e901f2cda7a6f9397f23194e641a5b83 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -3,6 +3,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
 
   before_action :require_lfs_enabled!
   before_action :lfs_check_access!
+  before_action :verify_workhorse_api!, only: [:upload_authorize]
 
   def download
     lfs_object = LfsObject.find_by_oid(oid)
@@ -15,14 +16,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
   end
 
   def upload_authorize
-    render(
-      json: {
-        StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
-        LfsOid: oid,
-        LfsSize: size,
-      },
-      content_type: 'application/json; charset=utf-8'
-    )
+    set_workhorse_internal_api_content_type
+    render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
   end
 
   def upload_finalize
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 139680d2df9e08b82a6aa700599006305dfdb39e..9f104d903cc8e6b1972d1d4d92840107ca9e53a1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,15 +9,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   before_action :module_enabled
   before_action :merge_request, only: [
-    :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
-    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
+    :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
+    :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
   ]
-  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
-  before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
+  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :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_commit_vars, only: [:diffs]
   before_action :define_diff_comment_vars, only: [:diffs]
-  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
+  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
+  before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
+  before_action :apply_diff_view_cookie!, only: [:new_diffs]
+  before_action :build_merge_request, only: [:new, :new_diffs]
 
   # Allow read any merge_request
   before_action :authorize_read_merge_request!
@@ -28,22 +31,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   # Allow modify merge_request
   before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
 
-  def index
-    terms = params['issue_search']
-    @merge_requests = merge_requests_collection
+  before_action :authenticate_user!, only: [:assign_related_issues]
 
-    if terms.present?
-      if terms =~ /\A[#!](\d+)\z/
-        @merge_requests = @merge_requests.where(iid: $1)
-      else
-        @merge_requests = @merge_requests.full_search(terms)
-      end
-    end
+  before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
 
+  def index
+    @merge_requests = merge_requests_collection
     @merge_requests = @merge_requests.page(params[:page])
     @merge_requests = @merge_requests.preload(:target_project)
 
-    @labels = @project.labels.where(title: params[:label_name])
+    if params[:label_name].present?
+      labels_params = { project_id: @project.id, title: params[:label_name] }
+      @labels = LabelsFinder.new(current_user, labels_params).execute
+    end
 
     respond_to do |format|
       format.html
@@ -81,12 +81,33 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   def diffs
     apply_diff_view_cookie!
 
-    @merge_request_diff = @merge_request.merge_request_diff
+    @merge_request_diff =
+      if params[:diff_id]
+        @merge_request.merge_request_diffs.find(params[:diff_id])
+      else
+        @merge_request.merge_request_diff
+      end
+
+    @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+    @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+    if params[:start_sha].present?
+      @start_sha = params[:start_sha]
+      @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+      unless @start_version
+        render_404
+      end
+    end
 
     respond_to do |format|
       format.html { define_discussion_vars }
       format.json do
-        @diffs = @merge_request.diffs(diff_options)
+        if @start_sha
+          compared_diff_version
+        else
+          original_diff_version
+        end
 
         render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
       end
@@ -130,6 +151,57 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     end
   end
 
+  def conflicts
+    respond_to do |format|
+      format.html { define_discussion_vars }
+
+      format.json do
+        if @merge_request.conflicts_can_be_resolved_in_ui?
+          render json: @merge_request.conflicts
+        elsif @merge_request.can_be_merged?
+          render json: {
+            message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
+            type: 'error'
+          }
+        else
+          render json: {
+            message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
+            type: 'error'
+          }
+        end
+      end
+    end
+  end
+
+  def conflict_for_path
+    return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+
+    file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path])
+
+    return render_404 unless file
+
+    render json: file, full_content: true
+  end
+
+  def resolve_conflicts
+    return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+
+    if @merge_request.can_be_merged?
+      render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
+      return
+    end
+
+    begin
+      MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
+
+      flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
+
+      render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
+    rescue Gitlab::Conflict::ResolutionError => e
+      render status: :bad_request, json: { message: e.message }
+    end
+  end
+
   def builds
     respond_to do |format|
       format.html do
@@ -141,29 +213,40 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     end
   end
 
-  def new
-    build_merge_request
-    @noteable = @merge_request
+  def pipelines
+    @pipelines = @merge_request.all_pipelines
 
-    @target_branches = if @merge_request.target_project
-                         @merge_request.target_project.repository.branch_names
-                       else
-                         []
-                       end
+    respond_to do |format|
+      format.html do
+        define_discussion_vars
 
-    @target_project = merge_request.target_project
-    @source_project = merge_request.source_project
-    @commits = @merge_request.compare_commits.reverse
-    @commit = @merge_request.diff_head_commit
-    @base_commit = @merge_request.diff_base_commit
-    @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
-    @diff_notes_disabled = true
+        render 'show'
+      end
+      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+    end
+  end
 
-    @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses.relevant if @pipeline
+  def new
+    define_new_vars
+  end
 
-    @note_counts = Note.where(commit_id: @commits.map(&:id)).
-      group(:commit_id).count
+  def new_diffs
+    respond_to do |format|
+      format.html do
+        define_new_vars
+        render "new"
+      end
+      format.json do
+        @diffs = if @merge_request.can_be_created
+                   @merge_request.diffs(diff_options)
+                 else
+                   []
+                 end
+        @diff_notes_disabled = true
+
+        render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+      end
+    end
   end
 
   def create
@@ -195,16 +278,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
                        @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 } })
+          render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
         end
       end
     else
       render "edit"
     end
+  rescue ActiveRecord::StaleObjectError
+    @conflict = true
+    render :edit
   end
 
   def remove_wip
-    MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
+    MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
 
     redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
       notice: "The merge request can now be merged."
@@ -237,8 +323,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       return
     end
 
-    TodoService.new.merge_merge_request(merge_request, current_user)
-
     @merge_request.update(merge_error: nil)
 
     if params[:merge_when_build_succeeds].present?
@@ -268,13 +352,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   def branch_from
     # This is always source
     @source_project = @merge_request.nil? ? @project : @merge_request.source_project
-    @commit = @repository.commit(params[:ref]) if params[:ref].present?
+
+    if params[:ref].present?
+      @ref = params[:ref]
+      @commit = @repository.commit(@ref)
+    end
+
     render layout: false
   end
 
   def branch_to
     @target_project = selected_target_project
-    @commit = @target_project.commit(params[:ref]) if params[:ref].present?
+
+    if params[:ref].present?
+      @ref = params[:ref]
+      @commit = @target_project.commit(@ref)
+    end
+
     render layout: false
   end
 
@@ -285,6 +379,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     render layout: false
   end
 
+  def assign_related_issues
+    result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
+
+    respond_to do |format|
+      format.html do
+        case result[:count]
+        when 0
+          flash[:error] = "Failed to assign you issues related to the merge request"
+        when 1
+          flash[:notice] = "1 issue has been assigned to you"
+        else
+          flash[:notice] = "#{result[:count]} issues have been assigned to you"
+        end
+
+        redirect_to(merge_request_path(@merge_request))
+      end
+    end
+  end
+
   def ci_status
     pipeline = @merge_request.pipeline
     if pipeline
@@ -295,7 +408,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
       status ||= "preparing"
     else
-      ci_service = @merge_request.source_project.ci_service
+      ci_service = @merge_request.source_project.try(:ci_service)
       status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
 
       if ci_service.respond_to?(:commit_coverage)
@@ -313,6 +426,36 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     render json: response
   end
 
+  def ci_environments_status
+    environments =
+      begin
+        @merge_request.environments.map do |environment|
+          next unless can?(current_user, :read_environment, environment)
+
+          project = environment.project
+          deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
+
+          stop_url =
+            if environment.stoppable? && can?(current_user, :create_deployment, environment)
+              stop_namespace_project_environment_path(project.namespace, project, environment)
+            end
+
+          {
+            id: environment.id,
+            name: environment.name,
+            url: namespace_project_environment_path(project.namespace, project, environment),
+            stop_url: stop_url,
+            external_url: environment.external_url,
+            external_url_formatted: environment.formatted_external_url,
+            deployed_at: deployment.try(:created_at),
+            deployed_at_formatted: deployment.try(:formatted_deployment_time)
+          }
+        end.compact
+      end
+
+    render json: environments
+  end
+
   protected
 
   def selected_target_project
@@ -324,7 +467,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   end
 
   def merge_request
-    @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
+    @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
   end
   alias_method :subscribable_resource, :merge_request
   alias_method :issuable, :merge_request
@@ -338,22 +481,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
   end
 
+  def authorize_can_resolve_conflicts!
+    return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
+  end
+
   def module_enabled
-    return render_404 unless @project.merge_requests_enabled
+    return render_404 unless @project.feature_available?(:merge_requests, current_user)
   end
 
   def validates_merge_request
-    # If source project was removed (Ex. mr from fork to origin)
-    return invalid_mr unless @merge_request.source_project
-
     # Show git not found page
     # if there is no saved commits between source & target branch
     if @merge_request.commits.blank?
       # and if target branch doesn't exist
       return invalid_mr unless @merge_request.target_branch_exists?
-
-      # or if source branch doesn't exist
-      return invalid_mr unless @merge_request.source_branch_exists?
     end
   end
 
@@ -361,25 +502,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @noteable = @merge_request
     @commits_count = @merge_request.commits.count
 
-    @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses.relevant if @pipeline
-
     if @merge_request.locked_long_ago?
       @merge_request.unlock_mr
       @merge_request.close
     end
+
+    define_pipelines_vars
   end
 
   # Discussion tab data is rendered on html responses of actions
   # :show, :diff, :commits, :builds. but not when request the data through AJAX
   def define_discussion_vars
     # Build a note object for comment form
-    @note = @project.notes.new(noteable: @noteable)
+    @note = @project.notes.new(noteable: @merge_request)
 
-    @discussions = @noteable.mr_and_commit_notes.
-      inc_author_project_award_emoji.
-      fresh.
-      discussions
+    @discussions = @merge_request.discussions
 
     preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
 
@@ -398,7 +535,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   def define_widget_vars
     @pipeline = @merge_request.pipeline
-    @pipelines = [@pipeline].compact
   end
 
   def define_commit_vars
@@ -412,8 +548,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       noteable_id: @merge_request.id
     }
 
-    @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
-    @grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions
+    @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
+    @grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
 
     Banzai::NoteRenderer.render(
       @grouped_diff_discussions.values.flat_map(&:notes),
@@ -425,18 +561,65 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     )
   end
 
+  def define_pipelines_vars
+    @pipelines = @merge_request.all_pipelines
+
+    if @pipelines.present?
+      @pipeline = @pipelines.first
+      @statuses = @pipeline.statuses.relevant
+    end
+  end
+
+  def define_new_vars
+    @noteable = @merge_request
+
+    @target_branches = if @merge_request.target_project
+                         @merge_request.target_project.repository.branch_names
+                       else
+                         []
+                       end
+
+    @target_project = merge_request.target_project
+    @source_project = merge_request.source_project
+    @commits = @merge_request.compare_commits.reverse
+    @commit = @merge_request.diff_head_commit
+    @base_commit = @merge_request.diff_base_commit
+
+    @note_counts = Note.where(commit_id: @commits.map(&:id)).
+      group(:commit_id).count
+
+    @labels = LabelsFinder.new(current_user, project_id: @project.id).execute
+
+    define_pipelines_vars
+  end
+
   def invalid_mr
-    # Render special view for MR with removed source or target branch
+    # Render special view for MR with removed target branch
     render 'invalid'
   end
 
   def merge_request_params
-    params.require(:merge_request).permit(
-      :title, :assignee_id, :source_project_id, :source_branch,
-      :target_project_id, :target_branch, :milestone_id,
-      :state_event, :description, :task_num, :force_remove_source_branch,
+    params.require(:merge_request)
+      .permit(merge_request_params_ce)
+  end
+
+  def merge_request_params_ce
+    [
+      :assignee_id,
+      :description,
+      :force_remove_source_branch,
+      :lock_version,
+      :milestone_id,
+      :source_branch,
+      :source_project_id,
+      :state_event,
+      :target_branch,
+      :target_project_id,
+      :task_num,
+      :title,
+
       label_ids: []
-    )
+    ]
   end
 
   def merge_params
@@ -456,6 +639,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   def build_merge_request
     params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
-    @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+    @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
+  end
+
+  def compared_diff_version
+    @diff_notes_disabled = true
+    @diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
+  end
+
+  def original_diff_version
+    @diff_notes_disabled = !@merge_request_diff.latest?
+    @diffs = @merge_request_diff.diffs(diff_options)
+  end
+
+  def close_merge_request_without_source_project
+    if !@merge_request.source_project && @merge_request.open?
+      @merge_request.close
+    end
   end
 end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index da2892bfb3f5e908833bec8f0069aa05e771d138..ff63f22cb5b047dd3dd4d242e0b96ff0d52821f9 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
   end
 
   def module_enabled
-    unless @project.issues_enabled || @project.merge_requests_enabled
+    unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
       return render_404
     end
   end
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 34318391dd909e3603ff5869dd61d336b2aae730..33a152ad34f86bdf2304a26e6afb089b4d112f1b 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -5,17 +5,29 @@ class Projects::NetworkController < Projects::ApplicationController
   before_action :require_non_empty_project
   before_action :assign_ref_vars
   before_action :authorize_download_code!
+  before_action :assign_commit
 
   def show
     @url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))
     @commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")
 
     respond_to do |format|
-      format.html
+      format.html do
+        if @options[:extended_sha1] && !@commit
+          flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+        end
+      end
 
       format.json do
         @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
       end
     end
   end
+
+  def assign_commit
+    return if params[:extended_sha1].blank?
+
+    @options[:extended_sha1] = params[:extended_sha1]
+    @commit = @repo.commit(@options[:extended_sha1])
+  end
 end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 766b7e9cf2228e0c8c03027bc2c9df87a205dc2f..0948ad2164929f8919af7bd4e713da29bf348170 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -5,6 +5,7 @@ class Projects::NotesController < Projects::ApplicationController
   before_action :authorize_read_note!
   before_action :authorize_create_note!, only: [:create]
   before_action :authorize_admin_note!, only: [:update, :destroy]
+  before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
   before_action :find_current_user_notes, only: [:index]
 
   def index
@@ -66,6 +67,33 @@ class Projects::NotesController < Projects::ApplicationController
     end
   end
 
+  def resolve
+    return render_404 unless note.resolvable?
+
+    note.resolve!(current_user)
+
+    MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+
+    discussion = note.discussion
+
+    render json: {
+      resolved_by: note.resolved_by.try(:name),
+      discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+    }
+  end
+
+  def unresolve
+    return render_404 unless note.resolvable?
+
+    note.unresolve!
+
+    discussion = note.discussion
+
+    render json: {
+      discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+    }
+  end
+
   private
 
   def note
@@ -125,7 +153,7 @@ class Projects::NotesController < Projects::ApplicationController
         id:     note.id,
         name:   note.name
       }
-    elsif note.valid?
+    elsif note.persisted?
       Banzai::NoteRenderer.render([note], @project, current_user)
 
       attrs = {
@@ -138,7 +166,7 @@ class Projects::NotesController < Projects::ApplicationController
       }
 
       if note.diff_note?
-        discussion = Discussion.new([note])
+        discussion = note.to_discussion
 
         attrs.merge!(
           diff_discussion_html: diff_discussion_html(discussion),
@@ -175,6 +203,10 @@ class Projects::NotesController < Projects::ApplicationController
     return access_denied! unless can?(current_user, :admin_note, note)
   end
 
+  def authorize_resolve_note!
+    return access_denied! unless can?(current_user, :resolve_note, note)
+  end
+
   def note_params
     params.require(:note).permit(
       :note, :noteable, :noteable_id, :noteable_type, :project_id,
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index b0c72cfe4b4fd6da6e4cb569eccb0b99ccecb359..533af80aee0ab929b9005975d972ad7c6a343db5 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController
 
   def index
     @scope = params[:scope]
-    all_pipelines = project.pipelines
-    @pipelines_count = all_pipelines.count
-    @running_or_pending_count = all_pipelines.running_or_pending.count
-    @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
-    @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
+    @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+
+    @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
+    @pipelines_count = PipelinesFinder.new(project).execute.count
   end
 
   def new
@@ -19,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController
   end
 
   def create
-    @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
+    @pipeline = Ci::CreatePipelineService
+      .new(project, current_user, create_params)
+      .execute(ignore_skip_ci: true, save_on_errors: false)
     unless @pipeline.persisted?
       render 'new'
       return
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a1189647e71c6eb2b20890f3caa86e6fffeb..699a56ae2f879ef3556d552285cde5288b2effe6 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -5,40 +5,35 @@ class Projects::ProjectMembersController < Projects::ApplicationController
   before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
 
   def index
+    @group_links = @project.project_group_links
+
     @project_members = @project.project_members
     @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
 
     if params[:search].present?
       users = @project.users.search(params[:search]).to_a
       @project_members = @project_members.where(user_id: users)
-    end
-
-    @project_members = @project_members.order('access_level DESC')
-
-    @group = @project.group
-
-    if @group
-      @group_members = @group.group_members
-      @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
-
-      if params[:search].present?
-        users = @group.users.search(params[:search]).to_a
-        @group_members = @group_members.where(user_id: users)
-      end
 
-      @group_members = @group_members.order('access_level DESC')
+      @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
     end
 
-    @requesters = @project.requesters if can?(current_user, :admin_project, @project)
+    @project_members = @project_members.order(access_level: :desc).page(params[:page])
+
+    @requesters = AccessRequestsFinder.new(@project).execute(current_user)
 
     @project_member = @project.project_members.new
-    @project_group_links = @project.project_group_links
   end
 
   def create
-    @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+    status = Members::CreateService.new(@project, current_user, params).execute
+
+    redirect_url = namespace_project_project_members_path(@project.namespace, @project)
 
-    redirect_to namespace_project_project_members_path(@project.namespace, @project)
+    if status
+      redirect_to redirect_url, notice: 'Users were successfully added.'
+    else
+      redirect_to redirect_url, alert: 'No users or groups specified.'
+    end
   end
 
   def update
@@ -50,10 +45,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
   end
 
   def destroy
-    @project_member = @project.members.find_by(id: params[:id]) ||
-      @project.requesters.find_by(id: params[:id])
-
-    Members::DestroyService.new(@project_member, current_user).execute
+    Members::DestroyService.new(@project, current_user, params).
+      execute(:all)
 
     respond_to do |format|
       format.html do
@@ -94,7 +87,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
   protected
 
   def member_params
-    params.require(:project_member).permit(:user_id, :access_level)
+    params.require(:project_member).permit(:user_id, :access_level, :expires_at)
   end
 
   # MembershipActions concern
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 6a227d85f6f5ce49eed364ba66104ded8942aeb9..97e6e9471e0a7167adb82c0d864639a1df8157a3 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -20,9 +20,8 @@ class Projects::ServicesController < Projects::ApplicationController
   def update
     if @service.update_attributes(service_params[:service])
       redirect_to(
-        edit_namespace_project_service_path(@project.namespace, @project,
-                                            @service.to_param, notice:
-                                            'Successfully updated.')
+        edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
+        notice: 'Successfully updated.'
       )
     else
       render 'edit'
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 6d0a7ee10317da6f66b3a24018f237fee8b0d1d6..e290a0eadda814488d8a3893abbe7f43e3a87f0b 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,8 @@
 class Projects::SnippetsController < Projects::ApplicationController
+  include ToggleAwardEmoji
+
   before_action :module_enabled
-  before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+  before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
 
   # Allow read any snippet
   before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
   def snippet
     @snippet ||= @project.snippets.find(params[:id])
   end
+  alias_method :awardable, :snippet
 
   def authorize_read_project_snippet!
     return render_404 unless can?(current_user, :read_project_snippet, @snippet)
@@ -94,7 +97,7 @@ class Projects::SnippetsController < Projects::ApplicationController
   end
 
   def module_enabled
-    return render_404 unless @project.snippets_enabled
+    return render_404 unless @project.feature_available?(:snippets, current_user)
   end
 
   def snippet_params
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 8592579abbd18a6174f6401e637fc6c135fabd23..953091492aeb8ede5892f8836a61594a5e93c431 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -1,4 +1,6 @@
 class Projects::TagsController < Projects::ApplicationController
+  include SortingHelper
+
   # Authorize
   before_action :require_non_empty_project
   before_action :authorize_download_code!
@@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController
   before_action :authorize_admin_project!, only: [:destroy]
 
   def index
-    @sort = params[:sort] || 'name'
-    @tags = @repository.tags_sorted_by(@sort)
+    params[:sort] = params[:sort].presence || 'name'
+
+    @sort = params[:sort]
+    @tags = TagsFinder.new(@repository, params).execute
     @tags = Kaminari.paginate_array(@tags).page(params[:page])
 
     @releases = project.releases.where(tag: @tags.map(&:name))
@@ -16,8 +20,10 @@ class Projects::TagsController < Projects::ApplicationController
   def show
     @tag = @repository.find_tag(params[:id])
 
+    return render_404 unless @tag
+
     @release = @project.releases.find_or_initialize_by(tag: @tag.name)
-    @commit = @repository.commit(@tag.target)
+    @commit = @repository.commit(@tag.dereferenced_target)
   end
 
   def create
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 47efbd4a93902efe1be30b1e30e0a5f711dc8113..a8a18b4fa1673c7a48a9c6fd8aee27f564f12cca 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,9 +1,10 @@
 class ProjectsController < Projects::ApplicationController
+  include IssuableCollections
   include ExtractsPath
 
-  before_action :authenticate_user!, except: [:show, :activity, :refs]
-  before_action :project, except: [:new, :create]
-  before_action :repository, except: [:new, :create]
+  before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
+  before_action :project, except: [:index, :new, :create]
+  before_action :repository, except: [:index, :new, :create]
   before_action :assign_ref_vars, only: [:show], if: :repo_exists?
   before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
 
@@ -29,6 +30,8 @@ class ProjectsController < Projects::ApplicationController
     @project = ::Projects::CreateService.new(current_user, project_params).execute
 
     if @project.saved?
+      cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
+
       redirect_to(
         project_path(@project),
         notice: "Project '#{@project.name}' was successfully created."
@@ -103,16 +106,7 @@ class ProjectsController < Projects::ApplicationController
     respond_to do |format|
       format.html do
         @notification_setting = current_user.notification_settings_for(@project) if current_user
-
-        if @project.repository_exists?
-          if @project.empty_repo?
-            render 'projects/empty'
-          else
-            render :show
-          end
-        else
-          render 'projects/no_repo'
-        end
+        render_landing_page
       end
 
       format.atom do
@@ -134,10 +128,22 @@ class ProjectsController < Projects::ApplicationController
   end
 
   def autocomplete_sources
-    note_type = params['type']
-    note_id = params['type_id']
+    noteable =
+      case params[:type]
+      when 'Issue'
+        IssuesFinder.new(current_user, project_id: @project.id).
+          execute.find_by(iid: params[:type_id])
+      when 'MergeRequest'
+        MergeRequestsFinder.new(current_user, project_id: @project.id).
+          execute.find_by(iid: params[:type_id])
+      when 'Commit'
+        @project.commit(params[:type_id])
+      else
+        nil
+      end
+
     autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
-    participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
+    participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
 
     @suggestions = {
       emojis: Gitlab::AwardEmoji.urls,
@@ -145,7 +151,8 @@ class ProjectsController < Projects::ApplicationController
       milestones: autocomplete.milestones,
       mergerequests: autocomplete.merge_requests,
       labels: autocomplete.labels,
-      members: participants
+      members: participants,
+      commands: autocomplete.commands(noteable, params[:type])
     }
 
     respond_to do |format|
@@ -153,6 +160,13 @@ class ProjectsController < Projects::ApplicationController
     end
   end
 
+  def new_issue_address
+    return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
+
+    current_user.reset_incoming_email_token!
+    render json: { new_issue_address: @project.new_issue_address(current_user) }
+  end
+
   def archive
     return access_denied! unless can?(current_user, :archive_project, @project)
 
@@ -272,6 +286,27 @@ class ProjectsController < Projects::ApplicationController
 
   private
 
+  # Render project landing depending of which features are available
+  # So if page is not availble in the list it renders the next page
+  #
+  # pages list order: repository readme, wiki home, issues list, customize workflow
+  def render_landing_page
+    if @project.feature_available?(:repository, current_user)
+      return render 'projects/no_repo' unless @project.repository_exists?
+      render 'projects/empty' if @project.empty_repo?
+    else
+      if @project.wiki_enabled?
+        @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])
+      end
+
+      render :show
+    end
+  end
+
   def determine_layout
     if [:new, :create].include?(action_name.to_sym)
       'application'
@@ -290,18 +325,53 @@ class ProjectsController < Projects::ApplicationController
   end
 
   def project_params
-    params.require(:project).permit(
-      :name, :path, :description, :issues_tracker, :tag_list, :runners_token,
-      :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled,
-      :issues_tracker_id, :default_branch,
-      :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
-      :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
-      :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
-    )
+    params.require(:project)
+      .permit(project_params_ce)
+  end
+
+  def project_params_ce
+    [
+      :avatar,
+      :build_allow_git_fetch,
+      :build_coverage_regex,
+      :build_timeout_in_minutes,
+      :container_registry_enabled,
+      :default_branch,
+      :description,
+      :import_url,
+      :issues_tracker,
+      :issues_tracker_id,
+      :last_activity_at,
+      :lfs_enabled,
+      :name,
+      :namespace_id,
+      :only_allow_merge_if_all_discussions_are_resolved,
+      :only_allow_merge_if_build_succeeds,
+      :path,
+      :public_builds,
+      :request_access_enabled,
+      :runners_token,
+      :tag_list,
+      :visibility_level,
+
+      project_feature_attributes: %i[
+        builds_access_level
+        issues_access_level
+        merge_requests_access_level
+        repository_access_level
+        snippets_access_level
+        wiki_access_level
+      ]
+    ]
   end
 
   def repo_exists?
-    project.repository_exists? && !project.empty_repo?
+    project.repository_exists? && !project.empty_repo? && project.repo
+
+  rescue Gitlab::Git::Repository::NoRepository
+    project.repository.expire_exists_cache
+
+    false
   end
 
   def project_view_files?
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 61517d21f9fb4f9fd57e95220f684dd9ea944a1e..b666aa01d6ba84e3942a24cf49d8dab9aac02dc3 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,8 +6,6 @@ class SearchController < ApplicationController
   layout 'search'
 
   def show
-    return if params[:search].nil? || params[:search].blank?
-
     if params[:project_id].present?
       @project = Project.find_by(id: params[:project_id])
       @project = nil unless can?(current_user, :download_code, @project)
@@ -18,6 +16,8 @@ class SearchController < ApplicationController
       @group = nil unless can?(current_user, :read_group, @group)
     end
 
+    return if params[:search].blank?
+
     @search_term = params[:search]
 
     @scope = params[:scope]
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 7271c933b9b810bc63d83ab38076addeed18faef..3085ff33aba62536a47e1fe1acafd19b8a48b635 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController
 
   def unsubscribe
     @sent_notification = SentNotification.for(params[:id])
+
     return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+    return unsubscribe_and_redirect if current_user || params[:force]
+  end
 
+  private
+
+  def unsubscribe_and_redirect
     noteable = @sent_notification.noteable
     noteable.unsubscribe(@sent_notification.recipient)
 
     flash[:notice] = "You have been unsubscribed from this thread."
+
     if current_user
       case noteable
       when Issue
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2a17c1f34db2825e9fb35a809fc91fdbef114da1..dee57e4a388fc2cb05172a914a4d9878c39aef7e 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,8 +1,10 @@
 class SnippetsController < ApplicationController
-  before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+  include ToggleAwardEmoji
+
+  before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
 
   # Allow read snippet
-  before_action :authorize_read_snippet!, only: [:show, :raw]
+  before_action :authorize_read_snippet!, only: [:show, :raw, :download]
 
   # Allow modify snippet
   before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -10,7 +12,7 @@ class SnippetsController < ApplicationController
   # Allow destroy snippet
   before_action :authorize_admin_snippet!, only: [:destroy]
 
-  skip_before_action :authenticate_user!, only: [:index, :show, :raw]
+  skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
 
   layout 'snippets'
   respond_to :html
@@ -73,6 +75,14 @@ class SnippetsController < ApplicationController
     )
   end
 
+  def download
+    send_data(
+      @snippet.content,
+      type: 'text/plain; charset=utf-8',
+      filename: @snippet.sanitized_file_name
+    )
+  end
+
   protected
 
   def snippet
@@ -85,6 +95,7 @@ class SnippetsController < ApplicationController
                    PersonalSnippet.find(params[:id])
                  end
   end
+  alias_method :awardable, :snippet
 
   def authorize_read_snippet!
     authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a99632454d94f354a92b0b607c127b9734ba9158..c4508ccc3b9c7af95bdba9b7a9335660c8af1609 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,6 @@
 class UsersController < ApplicationController
   skip_before_action :authenticate_user!
-  before_action :user
+  before_action :user, except: [:exists]
   before_action :authorize_read_user!, only: [:show]
 
   def show
@@ -65,7 +65,7 @@ class UsersController < ApplicationController
       format.html { render 'show' }
       format.json do
         render json: {
-          html: view_to_html_string("snippets/_snippets", collection: @snippets)
+          html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
         }
       end
     end
@@ -73,7 +73,7 @@ class UsersController < ApplicationController
 
   def calendar
     calendar = contributions_calendar
-    @timestamps = calendar.timestamps
+    @activity_dates = calendar.activity_dates
 
     render 'calendar', layout: false
   end
@@ -85,6 +85,10 @@ class UsersController < ApplicationController
     render 'calendar_activities', layout: false
   end
 
+  def exists
+    render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+  end
+
   private
 
   def authorize_read_user!
@@ -100,8 +104,7 @@ class UsersController < ApplicationController
   end
 
   def contributions_calendar
-    @contributions_calendar ||= Gitlab::ContributionsCalendar.
-      new(contributed_projects, user)
+    @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
   end
 
   def load_events
diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6ee49df99b97d66296603a4b7c64efb4bfa8844
--- /dev/null
+++ b/app/finders/access_requests_finder.rb
@@ -0,0 +1,27 @@
+class AccessRequestsFinder
+  attr_accessor :source
+
+  # Arguments:
+  #   source - a Group or Project
+  def initialize(source)
+    @source = source
+  end
+
+  def execute(*args)
+    execute!(*args)
+  rescue Gitlab::Access::AccessDeniedError
+    []
+  end
+
+  def execute!(current_user)
+    raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user)
+
+    source.requesters
+  end
+
+  private
+
+  def can_see_access_requests?(current_user)
+    source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
+  end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 33daac0399e29551b63af478c056d0a2e6d7d2b8..6297b2db369080c9e2fc0dddf367ba717164236e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -61,31 +61,26 @@ class IssuableFinder
   def project
     return @project if defined?(@project)
 
-    if project?
-      @project = Project.find(params[:project_id])
+    project = Project.find(params[:project_id])
+    project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
 
-      unless Ability.abilities.allowed?(current_user, :read_project, @project)
-        @project = nil
-      end
-    else
-      @project = nil
-    end
-
-    @project
+    @project = project
   end
 
   def projects
     return @projects if defined?(@projects)
+    return @projects = project if project?
 
-    if project?
-      @projects = project
-    elsif current_user && params[:authorized_only].presence && !current_user_related?
-      @projects = current_user.authorized_projects.reorder(nil)
-    elsif group
-      @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
-    else
-      @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
-    end
+    projects =
+      if current_user && params[:authorized_only].presence && !current_user_related?
+        current_user.authorized_projects
+      elsif group
+        GroupProjectsFinder.new(group).execute(current_user)
+      else
+        ProjectsFinder.new.execute(current_user)
+      end
+
+    @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
   end
 
   def search
@@ -124,15 +119,12 @@ class IssuableFinder
   def labels
     return @labels if defined?(@labels)
 
-    if labels? && !filter_by_no_label?
-      @labels = Label.where(title: label_names)
-
-      if projects
-        @labels = @labels.where(project: projects)
+    @labels =
+      if labels? && !filter_by_no_label?
+        LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
+      else
+        Label.none
       end
-    else
-      @labels = Label.none
-    end
   end
 
   def assignee?
@@ -183,17 +175,12 @@ class IssuableFinder
   end
 
   def by_state(items)
-    case params[:state]
-    when 'closed'
-      items.closed
-    when 'merged'
-      items.respond_to?(:merged) ? items.merged : items.closed
-    when 'all'
-      items
-    when 'opened'
-      items.opened
+    params[:state] ||= 'all'
+
+    if items.respond_to?(params[:state])
+      items.public_send(params[:state])
     else
-      raise 'You must specify default state'
+      items
     end
   end
 
@@ -216,7 +203,14 @@ class IssuableFinder
   end
 
   def by_search(items)
-    items = items.search(search) if search
+    if search
+      items =
+        if search =~ iid_pattern
+          items.where(iid: $~[:iid])
+        else
+          items.full_search(search)
+        end
+    end
 
     items
   end
@@ -272,8 +266,10 @@ class IssuableFinder
         items = items.without_label
       else
         items = items.with_label(label_names, params[:sort])
+
         if projects
-          items = items.where(labels: { project_id: projects })
+          label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
+          items = items.where(labels: { id: label_ids })
         end
       end
     end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index c2befa5a5b3a4837010ecc1c035f36437c2f56d8..be00a219205de287be0729417a12374b63cfc682 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder
   def init_collection
     Issue.visible_to_user(current_user)
   end
+
+  def iid_pattern
+    @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
+  end
 end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..865f093f04a1edf6d4fed16c693858fce031ab97
--- /dev/null
+++ b/app/finders/labels_finder.rb
@@ -0,0 +1,93 @@
+class LabelsFinder < UnionFinder
+  def initialize(current_user, params = {})
+    @current_user = current_user
+    @params = params
+  end
+
+  def execute(skip_authorization: false)
+    @skip_authorization = skip_authorization
+    items = find_union(label_ids, Label)
+    items = with_title(items)
+    sort(items)
+  end
+
+  private
+
+  attr_reader :current_user, :params, :skip_authorization
+
+  def label_ids
+    label_ids = []
+
+    if project
+      label_ids << project.group.labels if project.group.present?
+      label_ids << project.labels
+    else
+      label_ids << Label.where(group_id: projects.group_ids)
+      label_ids << Label.where(project_id: projects.select(:id))
+    end
+
+    label_ids
+  end
+
+  def sort(items)
+    items.reorder(title: :asc)
+  end
+
+  def with_title(items)
+    return items if title.nil?
+    return items.none if title.blank?
+
+    items.where(title: title)
+  end
+
+  def group_id
+    params[:group_id].presence
+  end
+
+  def project_id
+    params[:project_id].presence
+  end
+
+  def projects_ids
+    params[:project_ids]
+  end
+
+  def title
+    params[:title] || params[:name]
+  end
+
+  def project
+    return @project if defined?(@project)
+
+    if project_id
+      @project = find_project
+    else
+      @project = nil
+    end
+
+    @project
+  end
+
+  def find_project
+    if skip_authorization
+      Project.find_by(id: project_id)
+    else
+      available_projects.find_by(id: project_id)
+    end
+  end
+
+  def projects
+    return @projects if defined?(@projects)
+
+    @projects = skip_authorization ? Project.all : available_projects
+    @projects = @projects.in_namespace(group_id) if group_id
+    @projects = @projects.where(id: projects_ids) if projects_ids
+    @projects = @projects.reorder(nil)
+
+    @projects
+  end
+
+  def available_projects
+    @available_projects ||= ProjectsFinder.new.execute(current_user)
+  end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b258216d0d4b5368621b52115da5b38b49c3b16e..3b254e7d9d59fa400351dec5347f5905bb3a063e 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder
   def klass
     MergeRequest
   end
+
+  private
+
+  def iid_pattern
+    @iid_pattern ||= %r{\A[
+      #{Regexp.escape(MergeRequest.reference_prefix)}
+      #{Regexp.escape(Issue.reference_prefix)}
+      ](?<iid>\d+)\z
+    }x
+  end
 end
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
index 3334b8556df6e74e4521094e81a8bf14635df4ce..79eb45568bebf013d99fe7f1d9b0bb5a85acb12c 100644
--- a/app/finders/move_to_project_finder.rb
+++ b/app/finders/move_to_project_finder.rb
@@ -1,4 +1,6 @@
 class MoveToProjectFinder
+  PAGE_SIZE = 50
+
   def initialize(user)
     @user = user
   end
@@ -8,6 +10,10 @@ class MoveToProjectFinder
     projects = projects.search(search) if search.present?
     projects = projects.excluding_project(from_project)
 
+    # infinite scroll using offset
+    projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
+    projects = projects.limit(PAGE_SIZE)
+
     # to ask for Project#name_with_namespace
     projects.includes(namespace: :owner)
   end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 641fbf838f143d89cf68ed2ce4b875c91aabc5da..32aea75486deee72a465d8f5959a772d1787df79 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,30 +1,34 @@
 class PipelinesFinder
-  attr_reader :project
+  attr_reader :project, :pipelines
 
   def initialize(project)
     @project = project
+    @pipelines = project.pipelines
   end
 
-  def execute(pipelines, scope)
-    case scope
-    when 'running'
-      pipelines.running_or_pending
-    when 'branches'
-      from_ids(pipelines, ids_for_ref(pipelines, branches))
-    when 'tags'
-      from_ids(pipelines, ids_for_ref(pipelines, tags))
-    else
-      pipelines
-    end
+  def execute(scope: nil)
+    scoped_pipelines =
+      case scope
+      when 'running'
+        pipelines.running_or_pending
+      when 'branches'
+        from_ids(ids_for_ref(branches))
+      when 'tags'
+        from_ids(ids_for_ref(tags))
+      else
+        pipelines
+      end
+
+    scoped_pipelines.order(id: :desc)
   end
 
   private
 
-  def ids_for_ref(pipelines, refs)
+  def ids_for_ref(refs)
     pipelines.where(ref: refs).group(:ref).select('max(id)')
   end
 
-  def from_ids(pipelines, ids)
+  def from_ids(ids)
     pipelines.unscoped.where(id: ids)
   end
 
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b474f0805dcdddd9c15f060819e0b8dfbabcfeff
--- /dev/null
+++ b/app/finders/tags_finder.rb
@@ -0,0 +1,29 @@
+class TagsFinder
+  def initialize(repository, params)
+    @repository = repository
+    @params = params
+  end
+
+  def execute
+    tags = @repository.tags_sorted_by(sort)
+    filter_by_name(tags)
+  end
+
+  private
+
+  def sort
+    @params[:sort].presence
+  end
+
+  def search
+    @params[:search].presence
+  end
+
+  def filter_by_name(tags)
+    if search
+      tags.select { |tag| tag.name.include?(search) }
+    else
+      tags
+    end
+  end
+end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 4fe0070552efd8bc131559f63c20f248cd97db7a..a93a63bdb9b7892650063caf1763ae1360bfe817 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -17,7 +17,7 @@ class TodosFinder
 
   attr_accessor :current_user, :params
 
-  def initialize(current_user, params)
+  def initialize(current_user, params = {})
     @current_user = current_user
     @params = params
   end
@@ -33,7 +33,7 @@ class TodosFinder
     # the project IDs yielded by the todos query thus far
     items = by_project(items)
 
-    items.reorder(id: :desc)
+    sort(items)
   end
 
   private
@@ -83,7 +83,7 @@ class TodosFinder
     if project?
       @project = Project.find(params[:project_id])
 
-      unless Ability.abilities.allowed?(current_user, :read_project, @project)
+      unless Ability.allowed?(current_user, :read_project, @project)
         @project = nil
       end
     else
@@ -106,6 +106,10 @@ class TodosFinder
     params[:type]
   end
 
+  def sort(items)
+    params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+  end
+
   def by_action(items)
     if action?
       items = items.where(action: to_action_id)
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
deleted file mode 100644
index 81a12403801a4447a528228d5d204dc109a904b3..0000000000000000000000000000000000000000
--- a/app/finders/trending_projects_finder.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class TrendingProjectsFinder
-  def execute(current_user, start_date = 1.month.ago)
-    projects_for(current_user).trending(start_date)
-  end
-
-  private
-
-  def projects_for(current_user)
-    ProjectsFinder.new.execute(current_user)
-  end
-end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d27d30eaa3530b3d7f3d59e8864cc454eea1dc8
--- /dev/null
+++ b/app/helpers/accounts_helper.rb
@@ -0,0 +1,5 @@
+module AccountsHelper
+  def incoming_email_token_enabled?
+    current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation?
+  end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index e12a10529881930a6921d9470fa76989e086b7b9..16136d025304660408bfa13c733ab44ee4f26b28 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -16,7 +16,7 @@ module AppearancesHelper
   end
 
   def brand_text
-    markdown(brand_item.description)
+    markdown_field(brand_item, :description)
   end
 
   def brand_item
@@ -32,6 +32,8 @@ module AppearancesHelper
   end
 
   def custom_icon(icon_name, size: 16)
+    # We can't simply do the below, because there are some .erb SVGs.
+    #  File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
     render "shared/icons/#{icon_name}.svg", size: size
   end
 end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c3613bc67dd005804aec9d506140ffb7db3b8d51..c816b616631c7992513bb3399bf93d383206fb86 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -110,7 +110,7 @@ module ApplicationHelper
     project = event.project
 
     # Skip if project repo is empty or MR disabled
-    return false unless project && !project.empty_repo? && project.merge_requests_enabled
+    return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user)
 
     # Skip if user already created appropriate MR
     return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
@@ -151,7 +151,6 @@ module ApplicationHelper
   # time       - Time object
   # placement  - Tooltip placement String (default: "top")
   # html_class - Custom class for `time` element (default: "time_ago")
-  # skip_js    - When true, exclude the `script` tag (default: false)
   #
   # By default also includes a `script` element with Javascript necessary to
   # initialize the `timeago` jQuery extension. If this method is called many
@@ -163,22 +162,19 @@ module ApplicationHelper
   # `html_class` argument is provided.
   #
   # Returns an HTML-safe String
-  def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false)
+  def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
     css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
     css_classes << " #{html_class}" unless html_class.blank?
-    css_classes << ' js-timeago-pending' unless skip_js
 
     element = content_tag :time, time.to_s,
       class: css_classes,
-      datetime: time.to_time.getutc.iso8601,
       title: time.to_time.in_time_zone.to_s(:medium),
-      data: { toggle: 'tooltip', placement: placement, container: 'body' }
-
-    unless skip_js
-      element << javascript_tag(
-        "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
-      )
-    end
+      datetime: time.to_time.getutc.iso8601,
+      data: {
+        toggle: 'tooltip',
+        placement: placement,
+        container: 'body'
+      }
 
     element
   end
@@ -249,7 +245,7 @@ module ApplicationHelper
       milestone_title: params[:milestone_title],
       assignee_id: params[:assignee_id],
       author_id: params[:author_id],
-      issue_search: params[:issue_search],
+      search: params[:search],
       label_name: params[:label_name]
     }
 
@@ -280,32 +276,6 @@ module ApplicationHelper
     end
   end
 
-  def state_filters_text_for(entity, project)
-    titles = {
-      opened: "Open"
-    }
-
-    entity_title = titles[entity] || entity.to_s.humanize
-
-    count =
-      if project.nil?
-        nil
-      elsif current_controller?(:issues)
-        project.issues.visible_to_user(current_user).send(entity).count
-      elsif current_controller?(:merge_requests)
-        project.merge_requests.send(entity).count
-      end
-
-    html = content_tag :span, entity_title
-
-    if count.present?
-      html += " "
-      html += content_tag :span, number_with_delimiter(count), class: 'badge'
-    end
-
-    html.html_safe
-  end
-
   def truncate_first_line(message, length = 50)
     truncate(message.each_line.first.chomp, length: length) if message
   end
@@ -320,4 +290,8 @@ module ApplicationHelper
       capture(&block)
     end
   end
+
+  def page_class
+    "issue-boards-page" if current_controller?(:boards)
+  end
 end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 78c0b79d2bd82ca2def8a020f14768bc0c27cb46..45a567a1eba17bf8f2d5803e32fe920e7354eb0b 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
     current_application_settings.signin_enabled?
   end
 
-  def extra_sign_in_text
-    current_application_settings.sign_in_text
-  end
-
-  def after_sign_up_text
-    current_application_settings.after_sign_up_text
-  end
-
-  def shared_runners_text
-    current_application_settings.shared_runners_text
-  end
-
   def user_oauth_applications?
     current_application_settings.user_oauth_applications
   end
@@ -31,6 +19,10 @@ module ApplicationSettingsHelper
     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
@@ -101,11 +93,11 @@ module ApplicationSettingsHelper
     end
   end
 
-  def repository_storage_options_for_select
+  def repository_storages_options_for_select
     options = Gitlab.config.repositories.storages.map do |name, path|
       ["#{name} - #{path}", name]
     end
 
-    options_for_select(options, @application_setting.repository_storage)
+    options_for_select(options, @application_setting.repository_storages)
   end
 end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index aa8acbe7567585ae346b2d98ab509e91bc499ec8..b7e0ff8ecd097198dc11ca928f909379d8678e1c 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -4,17 +4,21 @@ module AvatarsHelper
       user: commit_or_event.author,
       user_name: commit_or_event.author_name,
       user_email: commit_or_event.author_email,
+      css_class: 'hidden-xs'
     }))
   end
 
   def user_avatar(options = {})
     avatar_size = options[:size] || 16
     user_name = options[:user].try(:name) || options[:user_name]
+    css_class = options[:css_class] || ''
+    
     avatar = image_tag(
       avatar_icon(options[:user] || options[:user_email], avatar_size),
-      class: "avatar has-tooltip hidden-xs s#{avatar_size}",
+      class: "avatar has-tooltip s#{avatar_size} #{css_class}",
       alt: "#{user_name}'s avatar",
-      title: user_name
+      title: user_name,
+      data: { container: 'body' }
     )
 
     if options[:user]
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..167b09e678f39f188a68177387516a3a81250109
--- /dev/null
+++ b/app/helpers/award_emoji_helper.rb
@@ -0,0 +1,12 @@
+module AwardEmojiHelper
+  def toggle_award_url(awardable)
+    return url_for([:toggle_award_emoji, awardable]) unless @project
+
+    if awardable.is_a?(Note)
+      # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
+      toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
+    else
+      url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+    end
+  end
+end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 1cb5d84762667f4a56027c0de847794b81f12e28..07ff6fb94889c58503a881861c64f31d3a18f805 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -11,17 +11,14 @@ module BlobHelper
   def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
     return unless current_user
 
-    blob = project.repository.blob_at(ref, path) rescue nil
+    blob = options.delete(:blob)
+    blob ||= project.repository.blob_at(ref, path) rescue nil
 
     return unless blob
 
-    from_mr = options[:from_merge_request_id]
-    link_opts = {}
-    link_opts[:from_merge_request_id] = from_mr if from_mr
-
     edit_path = namespace_project_edit_blob_path(project.namespace, project,
                                      tree_join(ref, path),
-                                     link_opts)
+                                     options[:link_opts])
 
     if !on_top_of_branch?(project, ref)
       button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
@@ -182,33 +179,6 @@ module BlobHelper
     }
   end
 
-  def selected_template(issuable)
-    templates = issuable_templates(issuable)
-    params[:issuable_template] if templates.include?(params[:issuable_template])
-  end
-
-  def can_add_template?(issuable)
-    names = issuable_templates(issuable)
-    names.empty? && can?(current_user, :push_code, @project) && !@project.private?
-  end
-
-  def merge_request_template_names
-    @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
-  end
-
-  def issue_template_names
-    @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
-  end
-
-  def issuable_templates(issuable)
-    @issuable_templates ||=
-      if issuable.is_a?(Issue)
-        issue_template_names
-      elsif issuable.is_a?(MergeRequest)
-        merge_request_template_names
-      end
-  end
-
   def ref_project
     @ref_project ||= @target_project || @project
   end
@@ -220,4 +190,12 @@ module BlobHelper
   def gitlab_ci_ymls
     @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
   end
+
+  def blob_editor_paths
+    {
+      'relative-url-root' => Rails.application.config.relative_url_root,
+      'assets-prefix' => Gitlab::Application.config.assets.prefix,
+      'blob-language' => @blob && @blob.language.try(:ace_mode)
+    }
+  end
 end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38c586ccd31e154720d191258f99ffe400a55e9f
--- /dev/null
+++ b/app/helpers/boards_helper.rb
@@ -0,0 +1,12 @@
+module BoardsHelper
+  def board_data
+    board = @board || @boards.first
+
+    {
+      endpoint: namespace_project_boards_path(@project.namespace, @project),
+      board_id: board.id,
+      disabled: "#{!can?(current_user, :admin_list, @project)}",
+      issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+    }
+  end
+end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 43a29c96bcaab292cd74c0c89fa1480836fd193e..eb03ced67eb609d47fad6b8a3232ca6fbff3b017 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
     return unless message.present?
 
     content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
-      icon('bullhorn') << ' ' << render_broadcast_message(message.message)
+      icon('bullhorn') << ' ' << render_broadcast_message(message)
     end
   end
 
@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
     end
   end
 
-  def render_broadcast_message(message)
-    Banzai.render(message, pipeline: :broadcast_message).html_safe
+  def render_broadcast_message(broadcast_message)
+    Banzai.render_field(broadcast_message, :message).html_safe
   end
 end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fde297c588ece6d367c04e3ce6a0a2d3fe9cb4b8
--- /dev/null
+++ b/app/helpers/builds_helper.rb
@@ -0,0 +1,18 @@
+module BuildsHelper
+  def sidebar_build_class(build, current_build)
+    build_class = ''
+    build_class += ' active' if build == current_build
+    build_class += ' retried' if build.retried?
+    build_class
+  end
+
+  def javascript_build_options
+    {
+      page_url: namespace_project_build_url(@project.namespace, @project, @build),
+      build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
+      build_status: @build.status,
+      build_stage: @build.stage,
+      state1: @build.trace_with_state[:state]
+    }
+  end
+end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index b478580978bfff79ec8df2190a4b5a3ea5e3599c..dee3c78df47a730b08b51d87d5737e81ba47c0f3 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -15,13 +15,15 @@ module ButtonHelper
   #
   # See http://clipboardjs.com/#usage
   def clipboard_button(data = {})
+    css_class = data[:class] || 'btn-clipboard btn-transparent'
+    title = data[:title] || 'Copy to Clipboard'
     data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
     content_tag :button,
       icon('clipboard'),
-      class: "btn btn-clipboard",
+      class: "btn #{css_class}",
       data: data,
       type: :button,
-      title: "Copy to Clipboard"
+      title: title
   end
 
   def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index ea2f5f9281a84c4ae5be1faadb37e70645f5e6cf..895c3d728ada8df77071cc68f7ca78ab18bae3d8 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -25,6 +25,11 @@ module CiStatusHelper
     end
   end
 
+  def ci_status_for_statuseable(subject)
+    status = subject.try(:status) || 'not found'
+    status.humanize
+  end
+
   def ci_icon_for_status(status)
     icon_name =
       case status
@@ -38,23 +43,37 @@ module CiStatusHelper
         'icon_status_pending'
       when 'running'
         'icon_status_running'
+      when 'play'
+        'icon_play'
+      when 'created'
+        'icon_status_created'
+      when 'skipped'
+        'icon_status_skipped'
       else
-        'icon_status_cancel'
+        'icon_status_canceled'
       end
 
     custom_icon(icon_name)
   end
 
-  def render_commit_status(commit, tooltip_placement: 'auto left')
+  def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
     project = commit.project
-    path = builds_namespace_project_commit_path(project.namespace, project, commit)
-    render_status_with_link('commit', commit.status, path, tooltip_placement)
+    path = pipelines_namespace_project_commit_path(
+      project.namespace,
+      project,
+      commit)
+
+    render_status_with_link(
+      'commit',
+      commit.status(ref),
+      path,
+      tooltip_placement: tooltip_placement)
   end
 
   def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
     project = pipeline.project
     path = namespace_project_pipeline_path(project.namespace, project, pipeline)
-    render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
+    render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
   end
 
   def no_runners_for_project?(project)
@@ -62,13 +81,17 @@ module CiStatusHelper
       Ci::Runner.shared.blank?
   end
 
-  private
+  def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
+    klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
+    title = "#{type.titleize}: #{ci_label_for_status(status)}"
+    data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
 
-  def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
-    link_to ci_icon_for_status(status),
-            path,
-            class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
-            title: "#{type.titleize}: #{ci_label_for_status(status)}",
-            data: { toggle: 'tooltip', placement: tooltip_placement }
+    if path
+      link_to ci_icon_for_status(status), path,
+              class: klass, title: title, data: data
+    else
+      content_tag :span, ci_icon_for_status(status),
+              class: klass, title: title, data: data
+    end
   end
 end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7a02d0b10d911b80fa97ac9ff8f8a09b4ca6e18e..ed402b698fb88a61c8f3f300507fe2831ae715dc 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -25,9 +25,11 @@ module CommitsHelper
     end
   end
 
-  def commit_to_html(commit, project, inline = true)
-    template = inline ? "inline_commit" : "commit"
-    render "projects/commits/#{template}", commit: commit, project: project unless commit.nil?
+  def commit_to_html(commit, ref, project)
+    render 'projects/commits/commit',
+      commit: commit,
+      ref: ref,
+      project: project
   end
 
   # Breadcrumb links for a Project and, if applicable, a tree path
@@ -98,28 +100,31 @@ module CommitsHelper
   end
 
   def link_to_browse_code(project, commit)
-    if current_controller?(:projects, :commits)
-      if @repo.blob_at(commit.id, @path)
-        return link_to(
-          "Browse File",
-          namespace_project_blob_path(project.namespace, project,
-                                      tree_join(commit.id, @path)),
-          class: "btn btn-default"
-        )
-      elsif @path.present?
-        return link_to(
-          "Browse Directory",
-          namespace_project_tree_path(project.namespace, project,
-                                      tree_join(commit.id, @path)),
-          class: "btn btn-default"
-        )
-      end
+    if @path.blank?
+      return link_to(
+        "Browse Files",
+        namespace_project_tree_path(project.namespace, project, commit),
+        class: "btn btn-default"
+      )
+    end
+
+    return unless current_controller?(:projects, :commits)
+
+    if @repo.blob_at(commit.id, @path)
+      return link_to(
+        "Browse File",
+        namespace_project_blob_path(project.namespace, project,
+                                    tree_join(commit.id, @path)),
+        class: "btn btn-default"
+      )
+    elsif @path.present?
+      return link_to(
+        "Browse Directory",
+        namespace_project_tree_path(project.namespace, project,
+                                    tree_join(commit.id, @path)),
+        class: "btn btn-default"
+      )
     end
-    link_to(
-      "Browse Files",
-      namespace_project_tree_path(project.namespace, project, commit),
-      class: "btn btn-default"
-    )
   end
 
   def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index f1dc906cab462204754cbe163b810881e2925541..aa54ee07bdccaf3b8ef34f2a7ef946e044724760 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
     from.present? &&
       to.present? &&
       from != to &&
-      project.merge_requests_enabled &&
+      project.feature_available?(:merge_requests, current_user) &&
       project.repository.branch_names.include?(from) &&
       project.repository.branch_names.include?(to)
   end
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8893209b3146f255e54a8e92a25e13b641570b31
--- /dev/null
+++ b/app/helpers/components_helper.rb
@@ -0,0 +1,9 @@
+module ComponentsHelper
+  def gitlab_workhorse_version
+    if request.headers['Gitlab-Workhorse'].present?
+      request.headers['Gitlab-Workhorse'].split('-').first
+    else
+      Gitlab::Workhorse.version
+    end
+  end
+end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 0725c3f4c56c812256d6021e3cb9978972e11633..f489f9aa0d6758239ef3177347e6091554c26d7a 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -51,12 +51,11 @@ module DiffHelper
     html.html_safe
   end
 
-  def diff_line_content(line, line_type = nil)
+  def diff_line_content(line)
     if line.blank?
-      " &nbsp;".html_safe
+      "&nbsp;".html_safe
     else
-      line[0] = ' ' if %w[new old].include?(line_type)
-      line
+      line.sub(/^[\-+ ]/, '').html_safe
     end
   end
 
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 4566f3782ccfb0858235226e9b5e1e5354f6d2f7..cbab1fd5967c90ea15f7c2d618b4abdc144c3442 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -40,9 +40,10 @@ module DropdownsHelper
   end
 
   def dropdown_toggle(toggle_text, data_attr, options = {})
+    default_label = data_attr[:default_label]
     content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
-      output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
-      output << icon('chevron-down')
+      output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
+      output << icon('caret-down')
       output.html_safe
     end
   end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index bfedcb1c42b62714b6ac6945589f721a10fed85d..00e640764080825ef1fd3e53d95a0b85a130a0af 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -39,6 +39,12 @@ module EventsHelper
     end
   end
 
+  def event_filter_visible(feature_key)
+    return true unless @project
+
+    @project.feature_available?(feature_key, current_user)
+  end
+
   def event_preposition(event)
     if event.push? || event.commented? || event.target
       "at"
@@ -154,7 +160,7 @@ module EventsHelper
   end
 
   def event_commit_title(message)
-    escape_once(truncate(message.split("\n").first, length: 70))
+    (message.split("\n").first || "").truncate(70)
   rescue
     "--broken encoding"
   end
diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb
index 096849552336c2495ab4d2f9fbe08c547f4737e0..8ab394384f30081ee9830a3e84cbc65e6144455e 100644
--- a/app/helpers/git_helper.rb
+++ b/app/helpers/git_helper.rb
@@ -2,4 +2,8 @@ module GitHelper
   def strip_gpg_signature(text)
     text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
   end
+
+  def short_sha(text)
+    Commit.truncate_sha(text)
+  end
 end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 1a259656f31a9e52b7eb24584ff459e95a371734..0772d848289fbfb025786d7eb8693b4cce611508 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
   def link_to_gfm(body, url, html_options = {})
     return "" if body.blank?
 
-    escaped_body = if body.start_with?('<img')
-                     body
-                   else
-                     escape_once(body)
-                   end
-
-    user = current_user if defined?(current_user)
-    gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
+    context = {
+      project: @project,
+      current_user: (current_user if defined?(current_user)),
+      pipeline: :single_line,
+    }
+    gfm_body = Banzai.render(body, context)
 
     fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
     if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
     context[:project] ||= @project
 
     html = Banzai.render(text, context)
+    banzai_postprocess(html, context)
+  end
 
-    context.merge!(
-      current_user:   (current_user if defined?(current_user)),
-
-      # RelativeLinkFilter
-      requested_path: @path,
-      project_wiki:   @project_wiki,
-      ref:            @ref
-    )
+  def markdown_field(object, field)
+    object = object.for_display if object.respond_to?(:for_display)
+    return "" unless object.present?
 
-    Banzai.post_process(html, context)
+    html = Banzai.render_field(object, field)
+    banzai_postprocess(html, object.banzai_render_context(field))
   end
 
   def asciidoc(text)
@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
       icon(options[:icon])
     end
   end
+
+  # Calls Banzai.post_process with some common context options
+  def banzai_postprocess(html, context)
+    context.merge!(
+      current_user:   (current_user if defined?(current_user)),
+
+      # RelativeLinkFilter
+      requested_path: @path,
+      project_wiki:   @project_wiki,
+      ref:            @ref
+    )
+
+    Banzai.post_process(html, context)
+  end
 end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 5386ddadc62bfe7efd8f20ad2a17f089fcdd8639..bccf64d1aac3154d6c3eaeb57ce5afa3817ec76e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -46,6 +46,10 @@ module GitlabRoutingHelper
     namespace_project_environments_path(project.namespace, project, *args)
   end
 
+  def project_cycle_analytics_path(project, *args)
+    namespace_project_cycle_analytics_path(project.namespace, project, *args)
+  end
+
   def project_builds_path(project, *args)
     namespace_project_builds_path(project.namespace, project, *args)
   end
@@ -66,6 +70,10 @@ module GitlabRoutingHelper
     namespace_project_runner_path(@project.namespace, @project, runner, *args)
   end
 
+  def environment_path(environment, *args)
+    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
@@ -86,6 +94,22 @@ module GitlabRoutingHelper
     namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
   end
 
+  def pipeline_url(pipeline, *args)
+    namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+  end
+
+  def pipeline_build_url(pipeline, build, *args)
+    namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
+  end
+
+  def commits_url(entity, *args)
+    namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args)
+  end
+
+  def commit_url(entity, *args)
+    namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args)
+  end
+
   def project_snippet_url(entity, *args)
     namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
   end
@@ -98,6 +122,14 @@ module GitlabRoutingHelper
     end
   end
 
+  def toggle_award_emoji_personal_snippet_path(*args)
+    toggle_award_emoji_snippet_path(*args)
+  end
+
+  def toggle_award_emoji_namespace_project_project_snippet_path(*args)
+    toggle_award_emoji_namespace_project_snippet_path(*args)
+  end
+
   ## Members
   def project_members_url(project, *args)
     namespace_project_project_members_url(project.namespace, project)
@@ -149,4 +181,20 @@ module GitlabRoutingHelper
   def resend_invite_group_member_path(group_member, *args)
     resend_invite_group_group_member_path(group_member.source, group_member)
   end
+
+  # Artifacts
+
+  def artifacts_action_path(path, project, build)
+    action, path_params = path.split('/', 2)
+    args = [project.namespace, project, build, path_params]
+
+    case action
+    when 'download'
+      download_namespace_project_build_artifacts_path(*args)
+    when 'browse'
+      browse_namespace_project_build_artifacts_path(*args)
+    when 'file'
+      file_namespace_project_build_artifacts_path(*args)
+    end
+  end
 end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b9211e884733f693e0905a3fbcc2c748564a244f..ab880ed6de0874ab773b016da8eb31c70bb80300 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -23,4 +23,29 @@ module GroupsHelper
       full_title
     end
   end
+
+  def projects_lfs_status(group)
+    lfs_status =
+      if group.lfs_enabled?
+        group.projects.select(&:lfs_enabled?).size
+      else
+        group.projects.reject(&:lfs_enabled?).size
+      end
+
+    size = group.projects.size
+
+    if lfs_status == size
+      'for all projects'
+    else
+      "for #{lfs_status} out of #{pluralize(size, 'project')}"
+    end
+  end
+
+  def group_lfs_status(group)
+    status = group.lfs_enabled? ? 'enabled' : 'disabled'
+
+    content_tag(:span, class: "lfs-#{status}") do
+      "#{status.humanize} #{projects_lfs_status(group)}"
+    end
+  end
 end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 109bc1a02d19bdbc8c8140aca3dc3e54d9af4bde..021d2b1471826dff1982c2784900ff13aefed4da 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,4 +1,9 @@
 module ImportHelper
+  def import_project_target(owner, name)
+    namespace = current_user.can_create_group? ? owner : current_user.namespace_path
+    "#{namespace}/#{name}"
+  end
+
   def github_project_link(path_with_namespace)
     link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
   end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 47d174361dbfdfd55f786bfe479c7330c13f15db..8127c3f3ee3e6006ef158667cc375418a9ba3b5c 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -8,18 +8,12 @@ module IssuablesHelper
   end
 
   def multi_label_name(current_labels, default_label)
-    # current_labels may be a string from before
-    if current_labels.is_a?(Array)
-      if current_labels.count > 1
-        "#{current_labels[0]} +#{current_labels.count - 1} more"
+    if current_labels && current_labels.any?
+      title = current_labels.first.try(:title)
+      if current_labels.size > 1
+        "#{title} +#{current_labels.size - 1} more"
       else
-        current_labels[0]
-      end
-    elsif current_labels.is_a?(String)
-      if current_labels.nil? || current_labels.empty?
-        default_label
-      else
-        current_labels
+        title
       end
     else
       default_label
@@ -36,6 +30,33 @@ module IssuablesHelper
     end
   end
 
+  def can_add_template?(issuable)
+    names = issuable_templates(issuable)
+    names.empty? && can?(current_user, :push_code, @project) && !@project.private?
+  end
+
+  def template_dropdown_tag(issuable, &block)
+    title = selected_template(issuable) || "Choose a template"
+    options = {
+      toggle_class: 'js-issuable-selector',
+      title: title,
+      filter: true,
+      placeholder: 'Filter',
+      footer_content: true,
+      data: {
+        data: issuable_templates(issuable),
+        field_name: 'issuable_template',
+        selected: selected_template(issuable),
+        project_path: ref_project.path,
+        namespace_path: ref_project.namespace.path
+      }
+    }
+
+    dropdown_tag(title, options: options) do
+      capture(&block)
+    end
+  end
+
   def user_dropdown_label(user_id, default_label)
     return default_label if user_id.nil?
     return "Unassigned" if user_id == "0"
@@ -49,6 +70,19 @@ module IssuablesHelper
     end
   end
 
+  def project_dropdown_label(project_id, default_label)
+    return default_label if project_id.nil?
+    return "Any project" if project_id == "0"
+
+    project = Project.find_by(id: project_id)
+
+    if project
+      project.name_with_namespace
+    else
+      default_label
+    end
+  end
+
   def milestone_dropdown_label(milestone_title, default_label = "Milestone")
     if milestone_title == Milestone::Upcoming.name
       milestone_title = Milestone::Upcoming.title
@@ -64,6 +98,14 @@ module IssuablesHelper
       author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
       author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
     end
+
+    if issuable.tasks?
+      output << "&ensp;".html_safe
+      output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs")
+      output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg")
+    end
+
+    output
   end
 
   def issuable_todo(issuable)
@@ -72,6 +114,33 @@ module IssuablesHelper
     end
   end
 
+  def issuable_labels_tooltip(labels, limit: 5)
+    first, last = labels.partition.with_index{ |_, i| i < limit  }
+
+    label_names = first.collect(&:name)
+    label_names << "and #{last.size} more" unless last.empty?
+
+    label_names.join(', ')
+  end
+
+  def issuables_state_counter_text(issuable_type, state)
+    titles = {
+      opened: "Open"
+    }
+
+    state_title = titles[state] || state.to_s.humanize
+
+    count =
+      Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
+        issuables_count_for_state(issuable_type, state)
+      end
+
+    html = content_tag(:span, state_title)
+    html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+
+    html.html_safe
+  end
+
   private
 
   def sidebar_gutter_collapsed?
@@ -89,4 +158,50 @@ module IssuablesHelper
       issuable.open? ? :opened : :closed
     end
   end
+
+  def issuable_filters_present
+    params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
+  end
+
+  def issuables_count_for_state(issuable_type, state)
+    issuables_finder = public_send("#{issuable_type}_finder")
+    issuables_finder.params[:state] = state
+
+    issuables_finder.execute.page(1).total_count
+  end
+
+  IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+  private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
+
+  def issuables_state_counter_cache_key(issuable_type, state)
+    opts = params.with_indifferent_access
+    opts[:state] = state
+    opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+
+    hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
+  end
+
+  def issuable_templates(issuable)
+    @issuable_templates ||=
+      case issuable
+      when Issue
+        issue_template_names
+      when MergeRequest
+        merge_request_template_names
+      else
+        raise 'Unknown issuable type!'
+      end
+  end
+
+  def merge_request_template_names
+    @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
+  end
+
+  def issue_template_names
+    @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
+  end
+
+  def selected_template(issuable)
+    params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
+  end
 end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 2e82b44437b2cf4b5e64cd648e2af4af37ea22ef..1644c346dd829b71adf4b47bff23ffd7b020d52d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -113,10 +113,17 @@ module IssuesHelper
     end
   end
 
-  def award_user_list(awards, current_user)
-    awards.map do |award|
-      award.user == current_user ? 'me' : award.user.name
-    end.join(', ')
+  def award_user_list(awards, current_user, limit: 10)
+    names = awards.map do |award|
+      award.user == current_user ? 'You' : award.user.name
+    end
+
+    current_user_name = names.delete('You')
+    names = names.insert(0, current_user_name).compact.first(limit)
+
+    names << "#{awards.size - names.size} more." if awards.size > names.size
+
+    names.to_sentence
   end
 
   def award_active_class(awards, current_user)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 5e9f5837101608e60e80a160736bec970e2989cd..221a84b042fc07c41ca39166046a5f2a04af70af 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -4,9 +4,8 @@ module LabelsHelper
   # Link to a Label
   #
   # label   - Label object to link to
-  # project - Project object which will be used as the context for the label's
-  #           link. If omitted, defaults to `@project`, or the label's own
-  #           project.
+  # subject - Project/Group object which will be used as the context for the
+  #           label's link. If omitted, defaults to the label's own group/project.
   # type    - The type of item the link will point to (:issue or
   #           :merge_request). If omitted, defaults to :issue.
   # block   - An optional block that will be passed to `link_to`, forming the
@@ -15,15 +14,14 @@ module LabelsHelper
   #
   # Examples:
   #
-  #   # Allow the generated link to use the label's own project
+  #   # Allow the generated link to use the label's own subject
   #   link_to_label(label)
   #
-  #   # Force the generated link to use @project
-  #   @project = Project.first
-  #   link_to_label(label)
+  #   # Force the generated link to use a provided group
+  #   link_to_label(label, subject: Group.last)
   #
   #   # Force the generated link to use a provided project
-  #   link_to_label(label, project: Project.last)
+  #   link_to_label(label, subject: Project.last)
   #
   #   # Force the generated link to point to merge requests instead of issues
   #   link_to_label(label, type: :merge_request)
@@ -32,9 +30,8 @@ module LabelsHelper
   #   link_to_label(label) { "My Custom Label Text" }
   #
   # Returns a String
-  def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
-    project ||= @project || label.project
-    link = label_filter_path(project, label, type: type)
+  def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
+    link = label_filter_path(subject || label.subject, label, type: type)
 
     if block_given?
       link_to link, class: css_class, &block
@@ -43,15 +40,40 @@ module LabelsHelper
     end
   end
 
-  def label_filter_path(project, label, type: issue)
-    send("namespace_project_#{type.to_s.pluralize}_path",
-                project.namespace,
-                project,
-                label_name: [label.name])
+  def label_filter_path(subject, label, type: :issue)
+    case subject
+    when Group
+      send("#{type.to_s.pluralize}_group_path",
+                  subject,
+                  label_name: [label.name])
+    when Project
+      send("namespace_project_#{type.to_s.pluralize}_path",
+                  subject.namespace,
+                  subject,
+                  label_name: [label.name])
+    end
+  end
+
+  def edit_label_path(label)
+    case label
+    when GroupLabel then edit_group_label_path(label.group, label)
+    when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
+    end
+  end
+
+  def destroy_label_path(label)
+    case label
+    when GroupLabel then group_label_path(label.group, label)
+    when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
+    end
   end
 
-  def project_label_names
-    @project.labels.pluck(:title)
+  def toggle_subscription_data(label)
+    return unless label.is_a?(ProjectLabel)
+
+    {
+      url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
+    }
   end
 
   def render_colored_label(label, label_suffix = '', tooltip: true)
@@ -68,8 +90,8 @@ module LabelsHelper
     span.html_safe
   end
 
-  def render_colored_cross_project_label(label, tooltip: true)
-    label_suffix = label.project.name_with_namespace
+  def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
+    label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
     label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
     render_colored_label(label, label_suffix, tooltip: tooltip)
   end
@@ -115,19 +137,36 @@ module LabelsHelper
   end
 
   def labels_filter_path
-    if @project
-      namespace_project_labels_path(@project.namespace, @project, :json)
+    return group_labels_path(@group, :json) if @group
+
+    project = @target_project || @project
+
+    if project
+      namespace_project_labels_path(project.namespace, project, :json)
     else
       dashboard_labels_path(:json)
     end
   end
 
   def label_subscription_status(label)
-    label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+    case label
+    when GroupLabel then 'Subscribing to group labels is currently not supported.'
+    when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+    end
   end
 
   def label_subscription_toggle_button_text(label)
-    label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+    case label
+    when GroupLabel then 'Subscribing to group labels is currently not supported.'
+    when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+    end
+  end
+
+  def label_deletion_confirm_text(label)
+    case label
+    when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
+    when ProjectLabel then 'Remove this label? Are you sure?'
+    end
   end
 
   # Required for Banzai::Filter::LabelReferenceFilter
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index eb651e3687eb64eb8d0eaf66e43faf7ad6e78128..d3966ba1f10d8c43a5cd5446d8ff4e098987a204 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,11 +1,13 @@
 module LfsHelper
+  include Gitlab::Routing.url_helpers
+
   def require_lfs_enabled!
     return if Gitlab.config.lfs.enabled
 
     render(
       json: {
         message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
-        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+        documentation_url: help_url,
       },
       status: 501
     )
@@ -23,18 +25,30 @@ module LfsHelper
   end
 
   def lfs_download_access?
-    project.public? || ci? || (user && user.can?(:download_code, project))
+    return false unless project.lfs_enabled?
+
+    ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+  end
+
+  def user_can_download_code?
+    has_authentication_ability?(:download_code) && can?(user, :download_code, project)
+  end
+
+  def build_can_download_code?
+    has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
   end
 
   def lfs_upload_access?
-    user && user.can?(:push_code, project)
+    return false unless project.lfs_enabled?
+
+    has_authentication_ability?(:push_code) && can?(user, :push_code, project)
   end
 
   def render_lfs_forbidden
     render(
       json: {
         message: 'Access forbidden. Check your access level.',
-        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+        documentation_url: help_url,
       },
       content_type: "application/vnd.git-lfs+json",
       status: 403
@@ -45,7 +59,7 @@ module LfsHelper
     render(
       json: {
         message: 'Not found.',
-        documentation_url: "#{Gitlab.config.gitlab.url}/help",
+        documentation_url: help_url,
       },
       content_type: "application/vnd.git-lfs+json",
       status: 404
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index db6e731c7446dde8a884c0b4bfe9d3eafad1deb8..a6659ea2fd1d0806d5da53b4bc7b7c51561e8df2 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -72,12 +72,29 @@ module MergeRequestsHelper
     )
   end
 
+  def mr_assign_issues_link
+    issues = MergeRequests::AssignIssuesService.new(@project,
+                                                    current_user,
+                                                    merge_request: @merge_request,
+                                                    closes_issues: mr_closes_issues
+                                                   ).assignable_issues
+    path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+    if issues.present?
+      pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
+      link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
+    end
+  end
+
   def source_branch_with_namespace(merge_request)
-    branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+    namespace = merge_request.source_project_namespace
+    branch = merge_request.source_branch
+
+    if merge_request.source_branch_exists?
+      namespace = link_to(namespace, project_path(merge_request.source_project))
+      branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+    end
 
     if merge_request.for_fork?
-      namespace = link_to(merge_request.source_project_namespace,
-        project_path(merge_request.source_project))
       namespace + ":" + branch
     else
       branch
@@ -98,6 +115,20 @@ module MergeRequestsHelper
   end
 
   def merge_request_button_visibility(merge_request, closed)
-    return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
+    return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
+  end
+
+  def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
+    diffs_namespace_project_merge_request_path(
+      project.namespace, project, merge_request,
+      diff_id: merge_request_diff.id, start_sha: start_sha)
+  end
+
+  def version_index(merge_request_diff)
+    @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
+  end
+
+  def different_base?(version1, version2)
+    version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
   end
 end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index b3e6e468ecd475b84f7ca502f64b64fd6442ad4e..83a2a4ad3ec498dc22e9097910c4a75f67767b1d 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -35,6 +35,30 @@ module MilestonesHelper
     milestone.issues.with_label(label.title).send(state).size
   end
 
+  # Returns count of milestones for different states
+  # Uses explicit hash keys as the 'opened' state URL params differs from the db value
+  # and we need to add the total
+  def milestone_counts(milestones)
+    counts = milestones.reorder(nil).group(:state).count
+
+    {
+      opened: counts['active'] || 0,
+      closed: counts['closed'] || 0,
+      all: counts.values.sum || 0
+    }
+  end
+
+  # Show 'active' class if provided GET param matches check
+  # `or_blank` allows the function to return 'active' when given an empty param
+  # Could be refactored to be simpler but that may make it harder to read
+  def milestone_class_for_state(param, check, match_blank_param = false)
+    if match_blank_param
+      'active' if param.blank? || param == check
+    else
+      'active' if param == check
+    end
+  end
+
   def milestone_progress_bar(milestone)
     options = {
       class: 'progress-bar progress-bar-success',
@@ -47,8 +71,9 @@ module MilestonesHelper
   end
 
   def milestones_filter_dropdown_path
-    if @project
-      namespace_project_milestones_path(@project.namespace, @project, :json)
+    project = @target_project || @project
+    if project
+      namespace_project_milestones_path(project.namespace, project, :json)
     else
       dashboard_milestones_path(:json)
     end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 94c6b548ecd1007faef2919f615761281af1b897..e0b8dc1393bb31c8630d6e1d0f887495d9624373 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,6 +1,9 @@
 module NamespacesHelper
-  def namespaces_options(selected = :current_user, display_path: false)
+  def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
     groups = current_user.owned_groups + current_user.masters_groups
+
+    groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
+
     users = [current_user.namespace]
 
     data_attr_group = { 'data-options-parent' => 'groups' }
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 3ff8be5e284cbe4126a16e309d46be7ce071973b..df87fac132def9e5305c7e6ed9ba134dcf5496f7 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,21 +1,7 @@
 module NavHelper
-  def nav_menu_collapsed?
-    cookies[:collapsed_nav] == 'true'
-  end
-
-  def nav_sidebar_class
-    if nav_menu_collapsed?
-      "sidebar-collapsed"
-    else
-      "sidebar-expanded"
-    end
-  end
-
   def page_sidebar_class
     if pinned_nav?
       "page-sidebar-expanded page-sidebar-pinned"
-    else
-      "page-sidebar-collapsed"
     end
   end
 
@@ -24,6 +10,8 @@ module NavHelper
       current_path?('merge_requests#diffs') ||
       current_path?('merge_requests#commits') ||
       current_path?('merge_requests#builds') ||
+      current_path?('merge_requests#conflicts') ||
+      current_path?('merge_requests#pipelines') ||
       current_path?('issues#show')
       if cookies[:collapsed_gutter] == 'true'
         "page-gutter right-sidebar-collapsed"
@@ -40,9 +28,7 @@ module NavHelper
     class_name << " with-horizontal-nav" if defined?(nav) && nav
 
     if pinned_nav?
-      class_name << " header-expanded header-pinned-nav"
-    else
-      class_name << " header-collapsed"
+      class_name << " header-sidebar-expanded header-sidebar-pinned"
     end
 
     class_name
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 26bde2230a95b10960d92d9913aa2ba2a5a83ad9..b0331f36a2f26eabcd6206f8ad79d3314311bafc 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -10,6 +10,10 @@ module NotesHelper
     Ability.can_edit_note?(current_user, note)
   end
 
+  def note_supports_slash_commands?(note)
+    Notes::SlashCommandsService.supported?(note, current_user)
+  end
+
   def noteable_json(noteable)
     {
       id: noteable.id,
@@ -49,7 +53,7 @@ module NotesHelper
     }
 
     if use_legacy_diff_note
-      discussion_id = LegacyDiffNote.build_discussion_id(
+      discussion_id = LegacyDiffNote.discussion_id(
         @comments_target[:noteable_type],
         @comments_target[:noteable_id] || @comments_target[:commit_id],
         line_code
@@ -60,7 +64,7 @@ module NotesHelper
         discussion_id: discussion_id
       )
     else
-      discussion_id = DiffNote.build_discussion_id(
+      discussion_id = DiffNote.discussion_id(
         @comments_target[:noteable_type],
         @comments_target[:noteable_id] || @comments_target[:commit_id],
         position
@@ -81,10 +85,8 @@ module NotesHelper
 
     data = discussion.reply_attributes.merge(line_type: line_type)
 
-    content_tag(:div, class: "discussion-reply-holder") do
-      button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
-                             data: data, title: 'Add a reply'
-    end
+    button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
+                           data: data, title: 'Add a reply'
   end
 
   def preload_max_access_for_authors(notes, project)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 7e8369d0a051b2d272981f597215981f69eceedb..03cc8f2b6bd9e3bd7df12c20299f5441c4b8227e 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -74,4 +74,13 @@ module NotificationsHelper
     return unless notification_setting.source_type
     hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
   end
+
+  def notification_event_name(event)
+    case event
+    when :success_pipeline
+      'Successful pipeline'
+    else
+      event.to_s.humanize
+    end
+  end
 end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 22387d664518a9a55e79448d29339da1837ae8c5..7d4d049101adc0c7b8cb0dd2421159301ec2262f 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -92,12 +92,8 @@ module PageLayoutHelper
     end
   end
 
-  def fluid_layout(enabled = false)
-    if @fluid_layout.nil?
-      @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
-    else
-      @fluid_layout
-    end
+  def fluid_layout
+    current_user && current_user.layout == "fluid"
   end
 
   def blank_container(enabled = false)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c3832cf5d65e0dbafe336b3d0fd97643cbb6ad11..a46f2c6e17d8be0e2f7db1876814b4851af9bf8b 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -50,6 +50,20 @@ module PreferencesHelper
   end
 
   def default_project_view
-    current_user ? current_user.project_view : 'readme'
+    return 'readme' unless current_user
+
+    user_view = current_user.project_view
+
+    if @project.feature_available?(:repository, current_user)
+      user_view
+    elsif user_view == "activity"
+      "activity"
+    elsif @project.wiki_enabled?
+      "wiki"
+    elsif @project.feature_available?(:issues, current_user)
+      "projects/issues/issues"
+    else
+      "customize_workflow"
+    end
   end
 end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 505545fbabb908d3e2fb686ab9d438f54fe2845c..42c00ec3cd5ad1e71e30fd710f142980307c51f1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -27,7 +27,7 @@ module ProjectsHelper
     author_html =  ""
 
     # Build avatar image tag
-    author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
+    author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
 
     # Build name span tag
     if opts[:by_username]
@@ -61,7 +61,9 @@ module ProjectsHelper
     project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
 
     if current_user
-      project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
+      project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do
+        icon("chevron-down")
+      end
     end
 
     full_title = "#{namespace_link} / #{project_link}".html_safe
@@ -116,8 +118,51 @@ module ProjectsHelper
     license.nickname || license.name
   end
 
+  def last_push_event
+    return unless current_user
+
+    project_ids = [@project.id]
+    if fork = current_user.fork_of(@project)
+      project_ids << fork.id
+    end
+
+    current_user.recent_push(project_ids)
+  end
+
+  def project_feature_access_select(field)
+    # Don't show option "everyone with access" if project is private
+    options = project_feature_options
+
+    if @project.private?
+      level = @project.project_feature.send(field)
+      options.delete('Everyone with access')
+      highest_available_option = options.values.max if level == ProjectFeature::ENABLED
+    end
+
+    options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
+
+    content_tag(
+      :select,
+      options,
+      name: "project[project_feature_attributes][#{field}]",
+      id: "project_project_feature_attributes_#{field}",
+      class: "pull-right form-control #{repo_children_classes(field)}",
+      data: { field: field }
+    ).html_safe
+  end
+
   private
 
+  def repo_children_classes(field)
+    needs_repo_check = [:merge_requests_access_level, :builds_access_level]
+    return unless needs_repo_check.include?(field)
+
+    classes = "project-repo-select js-repo-select"
+    classes << " disabled" unless @project.feature_available?(:repository, current_user)
+
+    classes
+  end
+
   def get_project_nav_tabs(project, current_user)
     nav_tabs = [:home]
 
@@ -176,6 +221,18 @@ module ProjectsHelper
     nav_tabs.flatten
   end
 
+  def project_lfs_status(project)
+    if project.lfs_enabled?
+      content_tag(:span, class: 'lfs-enabled') do
+        'Enabled'
+      end
+    else
+      content_tag(:span, class: 'lfs-disabled') do
+        'Disabled'
+      end
+    end
+  end
+
   def git_user_name
     if current_user
       current_user.name
@@ -236,6 +293,60 @@ module ProjectsHelper
     )
   end
 
+  def add_koding_stack_path(project)
+    namespace_project_new_blob_path(
+      project.namespace,
+      project,
+      project.default_branch || 'master',
+      file_name:      '.koding.yml',
+      commit_message: "Add Koding stack script",
+      content: <<-CONTENT.strip_heredoc
+        provider:
+          aws:
+            access_key: '${var.aws_access_key}'
+            secret_key: '${var.aws_secret_key}'
+        resource:
+          aws_instance:
+            #{project.path}-vm:
+              instance_type: t2.nano
+              user_data: |-
+
+                # Created by GitLab UI for :>
+
+                echo _KD_NOTIFY_@Installing Base packages...@
+
+                apt-get update -y
+                apt-get install git -y
+
+                echo _KD_NOTIFY_@Cloning #{project.name}...@
+
+                export KODING_USER=${var.koding_user_username}
+                export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
+                export BRANCH=${var.koding_queryString_branch}
+
+                sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
+
+                echo _KD_NOTIFY_@#{project.name} cloned.@
+      CONTENT
+    )
+  end
+
+  def koding_project_url(project = nil, branch = nil, sha = nil)
+    if project
+      import_path = "/Home/Stacks/import"
+
+      repo = project.path_with_namespace
+      branch ||= project.default_branch
+      sha ||= project.commit.short_id
+
+      path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
+
+      return URI.join(current_application_settings.koding_url, path).to_s
+    end
+
+    current_application_settings.koding_url
+  end
+
   def contribution_guide_path(project)
     if project && contribution_guide = project.repository.contribution_guide
       namespace_project_blob_path(
@@ -297,16 +408,6 @@ module ProjectsHelper
     namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
   end
 
-  def last_push_event
-    return unless current_user
-
-    if fork = current_user.fork_of(@project)
-      current_user.recent_push(fork.id)
-    else
-      current_user.recent_push(@project.id)
-    end
-  end
-
   def readme_cache_key
     sha = @project.commit.try(:sha) || 'nil'
     [@project.path_with_namespace, sha, "readme"].join('-')
@@ -345,4 +446,16 @@ module ProjectsHelper
 
     message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
   end
+
+  def project_feature_options
+    {
+      'Disabled' => ProjectFeature::DISABLED,
+      'Only team members' => ProjectFeature::PRIVATE,
+      'Everyone with access' => ProjectFeature::ENABLED
+    }
+  end
+
+  def project_child_container_class(view_path)
+    view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
+  end
 end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index c0195713f4a5090713f7243f5a6c96d18f4634b9..aba3a3f9c5deb39953d98fc43b5daf6cb36248dd 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -7,8 +7,10 @@ module SearchHelper
       projects_autocomplete(term)
     ].flatten
 
+    search_pattern = Regexp.new(Regexp.escape(term), "i")
+
     generic_results = project_autocomplete + default_autocomplete + help_autocomplete
-    generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
+    generic_results.select! { |result| result[:label] =~ search_pattern }
 
     [
       resources_results,
@@ -28,6 +30,37 @@ module SearchHelper
     "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
   end
 
+  def parse_search_result(result)
+    ref = nil
+    filename = nil
+    basename = nil
+    startline = 0
+
+    result.each_line.each_with_index do |line, index|
+      if line =~ /^.*:.*:\d+:/
+        ref, filename, startline = line.split(':')
+        startline = startline.to_i - index
+        extname = Regexp.escape(File.extname(filename))
+        basename = filename.sub(/#{extname}$/, '')
+        break
+      end
+    end
+
+    data = ""
+
+    result.each_line do |line|
+      data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+    end
+
+    OpenStruct.new(
+      filename: filename,
+      basename: basename,
+      ref: ref,
+      startline: startline,
+      data: data
+    )
+  end
+
   private
 
   # Autocomplete results for various settings pages
@@ -44,7 +77,7 @@ module SearchHelper
   def help_autocomplete
     [
       { category: "Help", label: "API Help",           url: help_page_path("api/README") },
-      { category: "Help", label: "Markdown Help",      url: help_page_path("markdown/markdown") },
+      { category: "Help", label: "Markdown Help",      url: help_page_path("user/markdown") },
       { category: "Help", label: "Permissions Help",   url: help_page_path("user/permissions") },
       { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
       { category: "Help", label: "Rake Tasks Help",    url: help_page_path("raketasks/README") },
@@ -120,8 +153,18 @@ module SearchHelper
     search_path(options)
   end
 
-  # Sanitize html generated after parsing markdown from issue description or comment
-  def search_md_sanitize(html)
+  # Sanitize a HTML field for search display. Most tags are stripped out and the
+  # maximum length is set to 200 characters.
+  def search_md_sanitize(object, field)
+    html = markdown_field(object, field)
+    html = Truncato.truncate(
+      html,
+      count_tags: false,
+      count_tail: false,
+      max_length: 200
+    )
+
+    # Truncato's filtered_tags and filtered_attributes are not quite the same
     sanitize(html, tags: %w(a p ol ul li pre code))
   end
 end
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 5f27e33c6ad5aef23ac6925d9fbf808a7b908165..8706876ae4a9afdcbf525650d0e3a4e42c079b22 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -49,12 +49,10 @@ module SelectsHelper
   end
 
   def select2_tag(id, opts = {})
-    css_class = ''
-    css_class << 'multiselect ' if opts[:multiple]
-    css_class << (opts[:class] || '')
+    opts[:class] << ' multiselect' if opts[:multiple]
     value = opts[:selected] || ''
 
-    hidden_field_tag(id, value, class: css_class)
+    hidden_field_tag(id, value, opts)
   end
 
   private
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d255df66a0c39214ea4ea9d2b66e5d36b6a44ed
--- /dev/null
+++ b/app/helpers/sentry_helper.rb
@@ -0,0 +1,9 @@
+module SentryHelper
+  def sentry_enabled?
+    Gitlab::Sentry.enabled?
+  end
+
+  def sentry_context
+    Gitlab::Sentry.context(current_user)
+  end
+end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 2dd0bf5d71e3f7205f2d846a954719f0b988fc2d..3d4abf76419bbecd74343c5534ad4c5448e79f89 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -8,7 +8,9 @@ module ServicesHelper
     when "note"
       "Event will be triggered when someone adds a comment"
     when "issue"
-      "Event will be triggered when an issue is created/updated/merged"
+      "Event will be triggered when an issue is created/updated/closed"
+    when "confidential_issue"
+      "Event will be triggered when a confidential issue is created/updated/closed"
     when "merge_request"
       "Event will be triggered when a merge request is created/updated/merged"
     when "build"
@@ -19,7 +21,7 @@ module ServicesHelper
   end
 
   def service_event_field_name(event)
-    event = event.pluralize if %w[merge_request issue].include?(event)
+    event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
     "#{event}_events"
   end
 end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..56749d80bd314e9fa9d97d80f1dfdb3e1a1992f2
--- /dev/null
+++ b/app/helpers/sidekiq_helper.rb
@@ -0,0 +1,19 @@
+module SidekiqHelper
+  SIDEKIQ_PS_REGEXP = /\A
+    (?<pid>\d+)\s+
+    (?<cpu>[\d\.,]+)\s+
+    (?<mem>[\d\.,]+)\s+
+    (?<state>[DRSTWXZNLsl\+<]+)\s+
+    (?<start>.+)\s+
+    (?<command>sidekiq.*\])\s*
+    \z/x
+
+  def parse_sidekiq_ps(line)
+    match = line.match(SIDEKIQ_PS_REGEXP)
+    if match
+      match[1..6]
+    else
+      %w[? ? ? ? ? ?]
+    end
+  end
+end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 0a5a8eb5aeec8de8f353858e304c2743d5587756..7e33a5620775641a918eb28c8a3135b5dff82552 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,10 +1,10 @@
 module SnippetsHelper
-  def reliable_snippet_path(snippet)
+  def reliable_snippet_path(snippet, opts = nil)
     if snippet.project_id?
       namespace_project_snippet_path(snippet.project.namespace,
-                                     snippet.project, snippet)
+                                     snippet.project, snippet, opts)
     else
-      snippet_path(snippet)
+      snippet_path(snippet, opts)
     end
   end
 
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index fb85544df2d7a9da130de651631bf962562cfd6a..c0ec1634cdb83b5cb004afcd8e06886892e430e1 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -3,6 +3,16 @@ module TagsHelper
     "/tags/#{tag}"
   end
 
+  def filter_tags_path(options = {})
+    exist_opts = {
+      search: params[:search],
+      sort: params[:sort]
+    }
+
+    options = exist_opts.merge(options)
+    namespace_project_tags_path(@project.namespace, @project, @id, options)
+  end
+
   def tag_list(project)
     html = ''
     project.tag_list.each do |tag|
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 790001222f12e92d47de0c72a201652647b08224..271e839692aab9fef18b6daf667020f40c871170 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -15,20 +15,9 @@ module TimeHelper
     "#{from.to_s(:short)} - #{to.to_s(:short)}"
   end
 
-  def duration_in_numbers(finished_at, started_at)
-    interval = interval_in_seconds(started_at, finished_at)
-    time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S"
+  def duration_in_numbers(duration)
+    time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
 
-    Time.at(interval).utc.strftime(time_format)
-  end
-
-  private
-
-  def interval_in_seconds(started_at, finished_at = nil)
-    if started_at && finished_at
-      finished_at.to_i - started_at.to_i
-    elsif started_at
-      Time.now.to_i - started_at.to_i
-    end
+    Time.at(duration).utc.strftime(time_format)
   end
 end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 0465327060ee1ee1a32c13bcc4e72e8532aa73db..09c6978679137bc1e69b71d448d8f8a4c5340819 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -61,6 +61,10 @@ module TodosHelper
     }
   end
 
+  def todos_filter_empty?
+    todos_filter_params.values.none?
+  end
+
   def todos_filter_path(options = {})
     without = options.delete(:without)
 
@@ -78,13 +82,11 @@ module TodosHelper
   end
 
   def todo_actions_options
-    actions = [
-      OpenStruct.new(id: '', title: 'Any Action'),
-      OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
-      OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
+    [
+      { id: '', text: 'Any Action' },
+      { id: Todo::ASSIGNED, text: 'Assigned' },
+      { id: Todo::MENTIONED, text: 'Mentioned' }
     ]
-
-    options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
   end
 
   def todo_projects_options
@@ -92,22 +94,48 @@ module TodosHelper
     projects = projects.includes(:namespace)
 
     projects = projects.map do |project|
-      OpenStruct.new(id: project.id, title: project.name_with_namespace)
+      { id: project.id, text: project.name_with_namespace }
     end
 
-    projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
-
-    options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
+    projects.unshift({ id: '', text: 'Any Project' }).to_json
   end
 
   def todo_types_options
-    types = [
-      OpenStruct.new(title: 'Any Type', name: ''),
-      OpenStruct.new(title: 'Issue', name: 'Issue'),
-      OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
+    [
+      { id: '', text: 'Any Type' },
+      { id: 'Issue', text: 'Issue' },
+      { id: 'MergeRequest', text: 'Merge Request' }
     ]
+  end
+
+  def todo_actions_dropdown_label(selected_action_id, default_action)
+    selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i}
+    selected_action ? selected_action[:text] : default_action
+  end
+
+  def todo_types_dropdown_label(selected_type, default_type)
+    selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' }
+    selected_type ? selected_type[:text] : default_type
+  end
 
-    options_from_collection_for_select(types, 'name', 'title', params[:type])
+  def todo_due_date(todo)
+    return unless todo.target.try(:due_date)
+
+    is_due_today = todo.target.due_date.today?
+    is_overdue = todo.target.overdue?
+    css_class =
+      if is_due_today
+        'text-warning'
+      elsif is_overdue
+        'text-danger'
+      else
+        ''
+      end
+
+    html = "&middot; ".html_safe
+    html << content_tag(:span, class: css_class) do
+      "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
+    end
   end
 
   private
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index d887cdadc3475a731a3b414528b33ec512a1d1ad..88f374be1e5c1340e6987c535ea8be7486802466 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -34,4 +34,8 @@ module WorkhorseHelper
     headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
     head :ok
   end
+
+  def set_workhorse_internal_api_content_type
+    headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+  end
 end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 8b83bbd93b74fc42d04bf4470f0d4d39586486e0..79c3c2e62c5a93c0b9db253183cd7d6ab8ce583f 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,6 +1,6 @@
 class BaseMailer < ActionMailer::Base
-  add_template_helper ApplicationHelper
-  add_template_helper GitlabMarkdownHelper
+  helper ApplicationHelper
+  helper GitlabMarkdownHelper
 
   attr_accessor :current_user
   helper_method :current_user, :can?
@@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base
   default reply_to: Proc.new { default_reply_to_address.format }
 
   def can?
-    Ability.abilities.allowed?(current_user, action, subject)
+    Ability.allowed?(current_user, action, subject)
   end
 
   private
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 415f6e12885805bb38fd91f9883760e5b64dea39..f7ed61625f43c43c2ba8c3ae4a1c13f1ffdad594 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
   default reply_to: Gitlab.config.gitlab.email_reply_to
 
   layout 'devise_mailer'
+
+  protected
+
+  def subject_for(key)
+    subject = super
+    subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
+    subject
+  end
 end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 6f54c42146c6d09f961ebf904adc1b89077bc596..d64e48f774b92123ceb195424fcce8e7de268154 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,11 @@ module Emails
       mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
     end
 
+    def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id)
+      setup_issue_mail(issue_id, recipient_id)
+      mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+    end
+
     def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
       setup_issue_mail(issue_id, recipient_id)
 
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 453116902937b23f9b191ab4b5d0f11adc2c6a6f..7b617b359eac991298740cf982091a6539e9a662 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -45,7 +45,7 @@ module Emails
       @token = token
 
       mail(to: member.invite_email,
-           subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+           subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
     end
 
     def member_invite_accepted_email(member_source_type, member_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 9dd11d20ea6b1129a98b3841108a74d3308a04fa..ec27ac517db361dd62693c29cc68168500168abd 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -6,6 +6,11 @@ module Emails
       mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
     end
 
+    def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
+      setup_merge_request_mail(merge_request_id, recipient_id)
+      mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+    end
+
     def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
       setup_merge_request_mail(merge_request_id, recipient_id)
 
@@ -42,6 +47,13 @@ module Emails
       mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
     end
 
+    def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id)
+      setup_merge_request_mail(merge_request_id, recipient_id)
+
+      @resolved_by = User.find(resolved_by_user_id)
+      mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id))
+    end
+
     private
 
     def setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9460a6cd2be9bc2edf15e180d2316313380303b8
--- /dev/null
+++ b/app/mailers/emails/pipelines.rb
@@ -0,0 +1,48 @@
+module Emails
+  module Pipelines
+    def pipeline_success_email(pipeline, recipients)
+      pipeline_mail(pipeline, recipients, 'succeeded')
+    end
+
+    def pipeline_failed_email(pipeline, recipients)
+      pipeline_mail(pipeline, recipients, 'failed')
+    end
+
+    private
+
+    def pipeline_mail(pipeline, recipients, status)
+      @project = pipeline.project
+      @pipeline = pipeline
+      @merge_request = pipeline.merge_requests.first
+      add_headers
+
+      # We use bcc here because we don't want to generate this emails for a
+      # thousand times. This could be potentially expensive in a loop, and
+      # recipients would contain all project watchers so it could be a lot.
+      mail(bcc: recipients,
+           subject: pipeline_subject(status),
+           skip_premailer: true) do |format|
+        format.html { render layout: false }
+        format.text
+      end
+    end
+
+    def add_headers
+      add_project_headers
+      add_pipeline_headers
+    end
+
+    def add_pipeline_headers
+      headers['X-GitLab-Pipeline-Id'] = @pipeline.id
+      headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref
+      headers['X-GitLab-Pipeline-Status'] = @pipeline.status
+    end
+
+    def pipeline_subject(status)
+      commit = @pipeline.short_sha
+      commit << " in #{@merge_request.to_reference}" if @merge_request
+
+      subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
+    end
+  end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cc709f68e46c0319f737f3faa7c590f20064671..0bc1c19e9cd3b5099ce2583853ced1f0868d95cb 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,14 +7,15 @@ class Notify < BaseMailer
   include Emails::Projects
   include Emails::Profile
   include Emails::Builds
+  include Emails::Pipelines
   include Emails::Members
 
-  add_template_helper MergeRequestsHelper
-  add_template_helper DiffHelper
-  add_template_helper BlobHelper
-  add_template_helper EmailsHelper
-  add_template_helper MembersHelper
-  add_template_helper GitlabRoutingHelper
+  helper MergeRequestsHelper
+  helper DiffHelper
+  helper BlobHelper
+  helper EmailsHelper
+  helper MembersHelper
+  helper GitlabRoutingHelper
 
   def test_email(recipient_email, subject, body)
     mail(to: recipient_email,
@@ -92,6 +93,7 @@ class Notify < BaseMailer
     subject = ""
     subject << "#{@project.name} | " if @project
     subject << extra.join(' | ') if extra.present?
+    subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
     subject
   end
 
@@ -108,6 +110,12 @@ class Notify < BaseMailer
     headers["X-GitLab-#{model.class.name}-ID"] = model.id
     headers['X-GitLab-Reply-Key'] = reply_key
 
+    if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+      headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
+
+      @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+    end
+
     if Gitlab::IncomingEmail.enabled?
       address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
       address.display_name = @project.name_with_namespace
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d9113ffd99a6a3aa7dfb361734aee8fb4d0b45ad..fa8f8bc3a5f8dca53e3340e53f2a90c54a51a3d6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,34 +1,5 @@
 class Ability
   class << self
-    # rubocop: disable Metrics/CyclomaticComplexity
-    def allowed(user, subject)
-      return anonymous_abilities(user, subject) if user.nil?
-      return [] unless user.is_a?(User)
-      return [] if user.blocked?
-
-      abilities_by_subject_class(user: user, subject: subject)
-    end
-
-    def abilities_by_subject_class(user:, subject:)
-      case subject
-      when CommitStatus then commit_status_abilities(user, subject)
-      when Project then project_abilities(user, subject)
-      when Issue then issue_abilities(user, subject)
-      when Note then note_abilities(user, subject)
-      when ProjectSnippet then project_snippet_abilities(user, subject)
-      when PersonalSnippet then personal_snippet_abilities(user, subject)
-      when MergeRequest then merge_request_abilities(user, subject)
-      when Group then group_abilities(user, subject)
-      when Namespace then namespace_abilities(user, subject)
-      when GroupMember then group_member_abilities(user, subject)
-      when ProjectMember then project_member_abilities(user, subject)
-      when User then user_abilities
-      when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
-      when Ci::Runner then runner_abilities(user, subject)
-      else []
-      end.concat(global_abilities(user))
-    end
-
     # Given a list of users and a project this method returns the users that can
     # read the given project.
     def users_that_can_read_project(users, project)
@@ -61,347 +32,7 @@ class Ability
       issues.select { |issue| issue.visible_to_user?(user) }
     end
 
-    # List of possible abilities for anonymous user
-    def anonymous_abilities(user, subject)
-      if subject.is_a?(PersonalSnippet)
-        anonymous_personal_snippet_abilities(subject)
-      elsif subject.is_a?(ProjectSnippet)
-        anonymous_project_snippet_abilities(subject)
-      elsif subject.is_a?(CommitStatus)
-        anonymous_commit_status_abilities(subject)
-      elsif subject.is_a?(Project) || subject.respond_to?(:project)
-        anonymous_project_abilities(subject)
-      elsif subject.is_a?(Group) || subject.respond_to?(:group)
-        anonymous_group_abilities(subject)
-      elsif subject.is_a?(User)
-        anonymous_user_abilities
-      else
-        []
-      end
-    end
-
-    def anonymous_project_abilities(subject)
-      project = if subject.is_a?(Project)
-                  subject
-                else
-                  subject.project
-                end
-
-      if project && project.public?
-        rules = [
-          :read_project,
-          :read_wiki,
-          :read_label,
-          :read_milestone,
-          :read_project_snippet,
-          :read_project_member,
-          :read_merge_request,
-          :read_note,
-          :read_pipeline,
-          :read_commit_status,
-          :read_container_image,
-          :download_code
-        ]
-
-        # Allow to read builds by anonymous user if guests are allowed
-        rules << :read_build if project.public_builds?
-
-        # Allow to read issues by anonymous user if issue is not confidential
-        rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
-
-        rules - project_disabled_features_rules(project)
-      else
-        []
-      end
-    end
-
-    def anonymous_commit_status_abilities(subject)
-      rules = anonymous_project_abilities(subject.project)
-      # If subject is Ci::Build which inherits from CommitStatus filter the abilities
-      rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
-      rules
-    end
-
-    def anonymous_group_abilities(subject)
-      rules = []
-
-      group = if subject.is_a?(Group)
-                subject
-              else
-                subject.group
-              end
-
-      rules << :read_group if group.public?
-
-      rules
-    end
-
-    def anonymous_personal_snippet_abilities(snippet)
-      if snippet.public?
-        [:read_personal_snippet]
-      else
-        []
-      end
-    end
-
-    def anonymous_project_snippet_abilities(snippet)
-      if snippet.public?
-        [:read_project_snippet]
-      else
-        []
-      end
-    end
-
-    def anonymous_user_abilities
-      [:read_user] unless restricted_public_level?
-    end
-
-    def global_abilities(user)
-      rules = []
-      rules << :create_group if user.can_create_group
-      rules << :read_users_list
-      rules
-    end
-
-    def project_abilities(user, project)
-      rules = []
-      key = "/user/#{user.id}/project/#{project.id}"
-
-      RequestStore.store[key] ||= begin
-        # Push abilities on the users team role
-        rules.push(*project_team_rules(project.team, user))
-
-        owner = user.admin? ||
-                project.owner == user ||
-                (project.group && project.group.has_owner?(user))
-
-        if owner
-          rules.push(*project_owner_rules)
-        end
-
-        if project.public? || (project.internal? && !user.external?)
-          rules.push(*public_project_rules)
-
-          # Allow to read builds for internal projects
-          rules << :read_build if project.public_builds?
-
-          unless owner || project.team.member?(user) || project_group_member?(project, user)
-            rules << :request_access if project.request_access_enabled
-          end
-        end
-
-        if project.archived?
-          rules -= project_archived_rules
-        end
-
-        rules - project_disabled_features_rules(project)
-      end
-    end
-
-    def project_team_rules(team, user)
-      # Rules based on role in project
-      if team.master?(user)
-        project_master_rules
-      elsif team.developer?(user)
-        project_dev_rules
-      elsif team.reporter?(user)
-        project_report_rules
-      elsif team.guest?(user)
-        project_guest_rules
-      else
-        []
-      end
-    end
-
-    def public_project_rules
-      @public_project_rules ||= project_guest_rules + [
-        :download_code,
-        :fork_project,
-        :read_commit_status,
-        :read_pipeline,
-        :read_container_image
-      ]
-    end
-
-    def project_guest_rules
-      @project_guest_rules ||= [
-        :read_project,
-        :read_wiki,
-        :read_issue,
-        :read_label,
-        :read_milestone,
-        :read_project_snippet,
-        :read_project_member,
-        :read_merge_request,
-        :read_note,
-        :create_project,
-        :create_issue,
-        :create_note,
-        :upload_file
-      ]
-    end
-
-    def project_report_rules
-      @project_report_rules ||= project_guest_rules + [
-        :download_code,
-        :fork_project,
-        :create_project_snippet,
-        :update_issue,
-        :admin_issue,
-        :admin_label,
-        :read_commit_status,
-        :read_build,
-        :read_container_image,
-        :read_pipeline,
-        :read_environment,
-        :read_deployment
-      ]
-    end
-
-    def project_dev_rules
-      @project_dev_rules ||= project_report_rules + [
-        :admin_merge_request,
-        :update_merge_request,
-        :create_commit_status,
-        :update_commit_status,
-        :create_build,
-        :update_build,
-        :create_pipeline,
-        :update_pipeline,
-        :create_merge_request,
-        :create_wiki,
-        :push_code,
-        :create_container_image,
-        :update_container_image,
-        :create_environment,
-        :create_deployment
-      ]
-    end
-
-    def project_archived_rules
-      @project_archived_rules ||= [
-        :create_merge_request,
-        :push_code,
-        :push_code_to_protected_branches,
-        :update_merge_request,
-        :admin_merge_request
-      ]
-    end
-
-    def project_master_rules
-      @project_master_rules ||= project_dev_rules + [
-        :push_code_to_protected_branches,
-        :update_project_snippet,
-        :update_environment,
-        :update_deployment,
-        :admin_milestone,
-        :admin_project_snippet,
-        :admin_project_member,
-        :admin_merge_request,
-        :admin_note,
-        :admin_wiki,
-        :admin_project,
-        :admin_commit_status,
-        :admin_build,
-        :admin_container_image,
-        :admin_pipeline,
-        :admin_environment,
-        :admin_deployment
-      ]
-    end
-
-    def project_owner_rules
-      @project_owner_rules ||= project_master_rules + [
-        :change_namespace,
-        :change_visibility_level,
-        :rename_project,
-        :remove_project,
-        :archive_project,
-        :remove_fork_project,
-        :destroy_merge_request,
-        :destroy_issue
-      ]
-    end
-
-    def project_disabled_features_rules(project)
-      rules = []
-
-      unless project.issues_enabled
-        rules += named_abilities('issue')
-      end
-
-      unless project.merge_requests_enabled
-        rules += named_abilities('merge_request')
-      end
-
-      unless project.issues_enabled or project.merge_requests_enabled
-        rules += named_abilities('label')
-        rules += named_abilities('milestone')
-      end
-
-      unless project.snippets_enabled
-        rules += named_abilities('project_snippet')
-      end
-
-      unless project.wiki_enabled
-        rules += named_abilities('wiki')
-      end
-
-      unless project.builds_enabled
-        rules += named_abilities('build')
-        rules += named_abilities('pipeline')
-        rules += named_abilities('environment')
-        rules += named_abilities('deployment')
-      end
-
-      unless project.container_registry_enabled
-        rules += named_abilities('container_image')
-      end
-
-      rules
-    end
-
-    def group_abilities(user, group)
-      rules = []
-      rules << :read_group if can_read_group?(user, group)
-
-      owner = user.admin? || group.has_owner?(user)
-      master = owner || group.has_master?(user)
-
-      # Only group masters and group owners can create new projects
-      if master
-        rules += [
-          :create_projects,
-          :admin_milestones
-        ]
-      end
-
-      # Only group owner and administrators can admin group
-      if owner
-        rules += [
-          :admin_group,
-          :admin_namespace,
-          :admin_group_member,
-          :change_visibility_level
-        ]
-      end
-
-      if group.public? || (group.internal? && !user.external?)
-        rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
-      end
-
-      rules.flatten
-    end
-
-    def can_read_group?(user, group)
-      return true if user.admin?
-      return true if group.public?
-      return true if group.internal? && !user.external?
-      return true if group.users.include?(user)
-
-      GroupProjectsFinder.new(group).execute(user).any?
-    end
-
+    # TODO: make this private and use the actual abilities stuff for this
     def can_edit_note?(user, note)
       return false if !note.editable? || !user.present?
       return true if note.author == user || user.admin?
@@ -414,202 +45,23 @@ class Ability
       end
     end
 
-    def namespace_abilities(user, namespace)
-      rules = []
-
-      # Only namespace owner and administrators can admin it
-      if namespace.owner == user || user.admin?
-        rules += [
-          :create_projects,
-          :admin_namespace
-        ]
-      end
-
-      rules.flatten
-    end
-
-    [:issue, :merge_request].each do |name|
-      define_method "#{name}_abilities" do |user, subject|
-        rules = []
-
-        if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
-          rules += [
-            :"read_#{name}",
-            :"update_#{name}",
-          ]
-        end
-
-        rules += project_abilities(user, subject.project)
-        rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
-        rules
-      end
-    end
-
-    def note_abilities(user, note)
-      rules = []
-
-      if note.author == user
-        rules += [
-          :read_note,
-          :update_note,
-          :admin_note
-        ]
-      end
-
-      if note.respond_to?(:project) && note.project
-        rules += project_abilities(user, note.project)
-      end
-
-      rules
-    end
-
-    def personal_snippet_abilities(user, snippet)
-      rules = []
-
-      if snippet.author == user
-        rules += [
-          :read_personal_snippet,
-          :update_personal_snippet,
-          :admin_personal_snippet
-        ]
-      end
-
-      if snippet.public? || (snippet.internal? && !user.external?)
-        rules << :read_personal_snippet
-      end
-
-      rules
-    end
-
-    def project_snippet_abilities(user, snippet)
-      rules = []
-
-      if snippet.author == user || user.admin?
-        rules += [
-          :read_project_snippet,
-          :update_project_snippet,
-          :admin_project_snippet
-        ]
-      end
-
-      if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
-        rules << :read_project_snippet
-      end
-
-      rules
+    def allowed?(user, action, subject)
+      allowed(user, subject).include?(action)
     end
 
-    def group_member_abilities(user, subject)
-      rules = []
-      target_user = subject.user
-      group = subject.group
-
-      unless group.last_owner?(target_user)
-        can_manage = group_abilities(user, group).include?(:admin_group_member)
-
-        if can_manage
-          rules << :update_group_member
-          rules << :destroy_group_member
-        elsif user == target_user
-          rules << :destroy_group_member
-        end
-      end
-
-      rules
-    end
-
-    def project_member_abilities(user, subject)
-      rules = []
-      target_user = subject.user
-      project = subject.project
-
-      unless target_user == project.owner
-        can_manage = project_abilities(user, project).include?(:admin_project_member)
-
-        if can_manage
-          rules << :update_project_member
-          rules << :destroy_project_member
-        elsif user == target_user
-          rules << :destroy_project_member
-        end
-      end
-
-      rules
-    end
-
-    def commit_status_abilities(user, subject)
-      rules = project_abilities(user, subject.project)
-      # If subject is Ci::Build which inherits from CommitStatus filter the abilities
-      rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
-      rules
-    end
-
-    def filter_build_abilities(rules)
-      # If we can't read build we should also not have that
-      # ability when looking at this in context of commit_status
-      %w(read create update admin).each do |rule|
-        rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
-      end
-      rules
-    end
-
-    def runner_abilities(user, runner)
-      if user.is_admin?
-        [:assign_runner]
-      elsif runner.is_shared? || runner.locked?
-        []
-      elsif user.ci_authorized_runners.include?(runner)
-        [:assign_runner]
-      else
-        []
-      end
-    end
-
-    def user_abilities
-      [:read_user]
-    end
+    def allowed(user, subject)
+      return uncached_allowed(user, subject) unless RequestStore.active?
 
-    def abilities
-      @abilities ||= begin
-        abilities = Six.new
-        abilities << self
-        abilities
-      end
+      user_key = user ? user.id : 'anonymous'
+      subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+      key = "/ability/#{user_key}/#{subject_key}"
+      RequestStore[key] ||= uncached_allowed(user, subject).freeze
     end
 
     private
 
-    def restricted_public_level?
-      current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
-    end
-
-    def named_abilities(name)
-      [
-        :"read_#{name}",
-        :"create_#{name}",
-        :"update_#{name}",
-        :"admin_#{name}"
-      ]
-    end
-
-    def filter_confidential_issues_abilities(user, issue, rules)
-      return rules if user.admin? || !issue.confidential?
-
-      unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
-        rules.delete(:admin_issue)
-        rules.delete(:read_issue)
-        rules.delete(:update_issue)
-      end
-
-      rules
-    end
-
-    def project_group_member?(project, user)
-      project.group &&
-      (
-        project.group.members.exists?(user_id: user.id) ||
-        project.group.requesters.exists?(user_id: user.id)
-      )
+    def uncached_allowed(user, subject)
+      BasePolicy.class_for(subject).abilities(user, subject)
     end
   end
 end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index b01a244032d9de9bbed3594263ffa04b7ff92ecc..2340453831e42d5525e913f4aab39791318aa56f 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -1,4 +1,8 @@
 class AbuseReport < ActiveRecord::Base
+  include CacheMarkdownField
+
+  cache_markdown_field :message, pipeline: :single_line
+
   belongs_to :reporter, class_name: 'User'
   belongs_to :user
 
@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
   validates :message, presence: true
   validates :user_id, uniqueness: { message: 'has already been reported' }
 
+  # For CacheMarkdownField
+  alias_method :author, :reporter
+
   def remove_user(deleted_by:)
     user.block
     DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 4cf8dd9a8ce2f3b4d5519eb4646d347004a808fc..e4106e1c2e90f1d13807e23227071920ce0c57a8 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,4 +1,8 @@
 class Appearance < ActiveRecord::Base
+  include CacheMarkdownField
+
+  cache_markdown_field :description
+
   validates :title,       presence: true
   validates :description, presence: true
   validates :logo,        file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8c19d9dc9c8c8481ce8d29f7cbc9cca425d32be8..bb60cc8736cbc12b13f5bf0fc4b29ae132757fb2 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,5 +1,7 @@
 class ApplicationSetting < ActiveRecord::Base
+  include CacheMarkdownField
   include TokenAuthenticatable
+
   add_authentication_token_field :runners_registration_token
   add_authentication_token_field :health_check_access_token
 
@@ -16,6 +18,12 @@ class ApplicationSetting < ActiveRecord::Base
   serialize :disabled_oauth_sign_in_sources, Array
   serialize :domain_whitelist, Array
   serialize :domain_blacklist, Array
+  serialize :repository_storages
+
+  cache_markdown_field :sign_in_text
+  cache_markdown_field :help_page_text
+  cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
+  cache_markdown_field :after_sign_up_text
 
   attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
 
@@ -55,6 +63,10 @@ class ApplicationSetting < ActiveRecord::Base
             presence: true,
             if: :akismet_enabled
 
+  validates :koding_url,
+            presence: true,
+            if: :koding_enabled
+
   validates :max_attachment_size,
             presence: true,
             numericality: { only_integer: true, greater_than: 0 }
@@ -63,9 +75,8 @@ class ApplicationSetting < ActiveRecord::Base
             presence: true,
             numericality: { only_integer: true, greater_than: 0 }
 
-  validates :repository_storage,
-    presence: true,
-    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
+  validates :repository_storages, presence: true
+  validate :check_repository_storages
 
   validates :enabled_git_access_protocol,
             inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
@@ -74,6 +85,18 @@ class ApplicationSetting < ActiveRecord::Base
             presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
             if: :domain_blacklist_enabled?
 
+  validates :housekeeping_incremental_repack_period,
+            presence: true,
+            numericality: { only_integer: true, greater_than: 0 }
+
+  validates :housekeeping_full_repack_period,
+            presence: true,
+            numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period }
+
+  validates :housekeeping_gc_period,
+            presence: true,
+            numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
+
   validates_each :restricted_visibility_levels do |record, attr, value|
     unless value.nil?
       value.each do |level|
@@ -142,19 +165,26 @@ class ApplicationSetting < ActiveRecord::Base
       default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       domain_whitelist: Settings.gitlab['domain_whitelist'],
-      import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+      import_sources: Gitlab::ImportSources.values,
       shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
       max_artifacts_size: Settings.artifacts['max_size'],
       require_two_factor_authentication: false,
       two_factor_grace_period: 48,
       recaptcha_enabled: false,
       akismet_enabled: false,
+      koding_enabled: false,
+      koding_url: nil,
       repository_checks_enabled: true,
       disabled_oauth_sign_in_sources: [],
       send_user_confirmation_email: false,
       container_registry_token_expire_delay: 5,
-      repository_storage: 'default',
+      repository_storages: ['default'],
       user_default_external: false,
+      housekeeping_enabled: true,
+      housekeeping_bitmaps_enabled: true,
+      housekeeping_incremental_repack_period: 10,
+      housekeeping_full_repack_period: 50,
+      housekeeping_gc_period: 200,
     )
   end
 
@@ -188,6 +218,25 @@ class ApplicationSetting < ActiveRecord::Base
     self.domain_blacklist_raw = file.read
   end
 
+  def repository_storages
+    Array(read_attribute(:repository_storages))
+  end
+
+  # repository_storage is still required in the API. Remove in 9.0
+  def repository_storage
+    repository_storages.first
+  end
+
+  def repository_storage=(value)
+    self.repository_storages = [value]
+  end
+
+  # Choose one of the available repository storage options. Currently all have
+  # equal weighting.
+  def pick_repository_storage
+    repository_storages.sample
+  end
+
   def runners_registration_token
     ensure_runners_registration_token!
   end
@@ -195,4 +244,12 @@ class ApplicationSetting < ActiveRecord::Base
   def health_check_access_token
     ensure_health_check_access_token!
   end
+
+  private
+
+  def check_repository_storages
+    invalid = repository_storages - Gitlab.config.repositories.storages.keys
+    errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
+      invalid.empty?
+  end
 end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 12cc5aaafba1bbeb13a361e93d88f8bdce6264cb..ab92e82033544a8db5236490f81199f04c42ea96 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -22,6 +22,18 @@ class Blob < SimpleDelegator
     new(blob)
   end
 
+  # Returns the data of the blob.
+  #
+  # If the blob is a text based blob the content is converted to UTF-8 and any
+  # invalid byte sequences are replaced.
+  def data
+    if binary?
+      super
+    else
+      @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
+    end
+  end
+
   def no_highlighting?
     size && size > 1.megabyte
   end
diff --git a/app/models/board.rb b/app/models/board.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c56422914a944d05c4b0e568cfaff7fcbdb97865
--- /dev/null
+++ b/app/models/board.rb
@@ -0,0 +1,15 @@
+class Board < ActiveRecord::Base
+  belongs_to :project
+
+  has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
+
+  validates :project, presence: true
+
+  def backlog_list
+    lists.merge(List.backlog).take
+  end
+
+  def done_list
+    lists.merge(List.done).take
+  end
+end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 61498140f27da8480a5e484006af6d85a5f79f12..cb40f33932a7bf421e6df47e1cb6d3790812751a 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -1,6 +1,9 @@
 class BroadcastMessage < ActiveRecord::Base
+  include CacheMarkdownField
   include Sortable
 
+  cache_markdown_field :message, pipeline: :broadcast_message
+
   validates :message,   presence: true
   validates :starts_at, presence: true
   validates :ends_at,   presence: true
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4c84f4c21c54e42421134164caa31e803e3134c8..bf5f92f84629fd344fb5a0bd12d39372c4c2e55b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,7 +1,10 @@
 module Ci
   class Build < CommitStatus
-    belongs_to :runner, class_name: 'Ci::Runner'
-    belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+    include TokenAuthenticatable
+    include AfterCommitQueue
+
+    belongs_to :runner
+    belongs_to :trigger_request
     belongs_to :erased_by, class_name: 'User'
 
     serialize :options
@@ -23,7 +26,10 @@ module Ci
 
     acts_as_taggable
 
+    add_authentication_token_field :token
+
     before_save :update_artifacts_size, if: :artifacts_file_changed?
+    before_save :ensure_token
     before_destroy { project }
 
     after_create :execute_hooks
@@ -38,6 +44,7 @@ module Ci
         new_build.status = 'pending'
         new_build.runner_id = nil
         new_build.trigger_request_id = nil
+        new_build.token = nil
         new_build.save
       end
 
@@ -62,28 +69,27 @@ module Ci
           status_event: 'enqueue'
         )
         MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+        build.pipeline.mark_as_processable_after_stage(build.stage_idx)
         new_build
       end
     end
 
     state_machine :status do
       after_transition pending: :running do |build|
-        build.execute_hooks
+        build.run_after_commit do
+          BuildHooksWorker.perform_async(id)
+        end
       end
 
       after_transition any => [:success, :failed, :canceled] do |build|
-        build.update_coverage
-        build.execute_hooks
+        build.run_after_commit do
+          BuildFinishedWorker.perform_async(id)
+        end
       end
 
       after_transition any => [:success] do |build|
-        if build.environment.present?
-          service = CreateDeploymentService.new(build.project, build.user,
-                                                environment: build.environment,
-                                                sha: build.sha,
-                                                ref: build.ref,
-                                                tag: build.tag)
-          service.execute(build)
+        build.run_after_commit do
+          BuildSuccessWorker.perform_async(id)
         end
       end
     end
@@ -97,7 +103,7 @@ module Ci
     end
 
     def playable?
-      project.builds_enabled? && commands.present? && manual?
+      project.builds_enabled? && commands.present? && manual? && skipped?
     end
 
     def play(current_user = nil)
@@ -127,13 +133,17 @@ module Ci
       latest_builds.where('stage_idx < ?', stage_idx)
     end
 
-    def trace_html
-      trace_with_state[:html] || ''
+    def trace_html(**args)
+      trace_with_state(**args)[:html] || ''
     end
 
-    def trace_with_state(state = nil)
-      trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
-      trace_with_state || {}
+    def trace_with_state(state: nil, last_lines: nil)
+      trace_ansi = trace(last_lines: last_lines)
+      if trace_ansi.present?
+        Ci::Ansi2html.convert(trace_ansi, state)
+      else
+        {}
+      end
     end
 
     def timeout
@@ -147,6 +157,7 @@ module Ci
       variables += runner.predefined_variables if runner
       variables += project.container_registry_variables
       variables += yaml_variables
+      variables += user_variables
       variables += project.secret_variables
       variables += trigger_request.user_variables if trigger_request
       variables
@@ -171,7 +182,7 @@ module Ci
     end
 
     def repo_url
-      auth = "gitlab-ci-token:#{token}@"
+      auth = "gitlab-ci-token:#{ensure_token!}@"
       project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
         prefix + auth
       end
@@ -207,29 +218,34 @@ module Ci
       end
     end
 
+    def has_trace_file?
+      File.exist?(path_to_trace) || has_old_trace_file?
+    end
+
     def has_trace?
       raw_trace.present?
     end
 
-    def raw_trace
-      if File.file?(path_to_trace)
-        File.read(path_to_trace)
-      elsif project.ci_id && File.file?(old_path_to_trace)
-        # Temporary fix for build trace data integrity
-        File.read(old_path_to_trace)
+    def raw_trace(last_lines: nil)
+      if File.exist?(trace_file_path)
+        Gitlab::Ci::TraceReader.new(trace_file_path).
+          read(last_lines: last_lines)
       else
         # backward compatibility
         read_attribute :trace
       end
     end
 
-    def trace
-      trace = raw_trace
-      if project && trace.present? && project.runners_token.present?
-        trace.gsub(project.runners_token, 'xxxxxx')
-      else
-        trace
-      end
+    ##
+    # Deprecated
+    #
+    # This is a hotfix for CI build data integrity, see #4246
+    def has_old_trace_file?
+      project.ci_id && File.exist?(old_path_to_trace)
+    end
+
+    def trace(last_lines: nil)
+      hide_secrets(raw_trace(last_lines: last_lines))
     end
 
     def trace_length
@@ -242,6 +258,7 @@ module Ci
 
     def trace=(trace)
       recreate_trace_dir
+      trace = hide_secrets(trace)
       File.write(path_to_trace, trace)
     end
 
@@ -255,12 +272,22 @@ module Ci
     def append_trace(trace_part, offset)
       recreate_trace_dir
 
+      trace_part = hide_secrets(trace_part)
+
       File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
       File.open(path_to_trace, 'ab') do |f|
         f.write(trace_part)
       end
     end
 
+    def trace_file_path
+      if has_old_trace_file?
+        old_path_to_trace
+      else
+        path_to_trace
+      end
+    end
+
     def dir_to_trace
       File.join(
         Settings.gitlab_ci.builds_path,
@@ -322,12 +349,8 @@ module Ci
       )
     end
 
-    def token
-      project.runners_token
-    end
-
     def valid_token?(token)
-      project.valid_runners_token?(token)
+      self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
     end
 
     def has_tags?
@@ -416,6 +439,15 @@ module Ci
       read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || []
     end
 
+    def user_variables
+      return [] if user.blank?
+
+      [
+        { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+        { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+      ]
+    end
+
     private
 
     def update_artifacts_size
@@ -451,6 +483,7 @@ module Ci
       ]
       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
     end
 
@@ -459,5 +492,14 @@ module Ci
 
       pipeline.config_processor.build_attributes(name)
     end
+
+    def hide_secrets(trace)
+      return unless trace
+
+      trace = trace.dup
+      Ci::MaskSecret.mask!(trace, project.runners_token) if project
+      Ci::MaskSecret.mask!(trace, token)
+      trace
+    end
   end
 end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 130afeb724e2d2b18e7d18847a5d8a8fb7377e44..3fee6c187700ee57e17cefdcccf830170d161d2b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,23 +1,25 @@
 module Ci
   class Pipeline < ActiveRecord::Base
     extend Ci::Model
-    include Statuseable
+    include HasStatus
+    include Importable
+    include AfterCommitQueue
 
     self.table_name = 'ci_commits'
 
-    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+    belongs_to :project, foreign_key: :gl_project_id
     belongs_to :user
 
     has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
-    has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
-    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', 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
-    validates_presence_of :ref
-    validates_presence_of :status
-    validate :valid_commit_sha
+    validates_presence_of :sha, unless: :importing?
+    validates_presence_of :ref, unless: :importing?
+    validates_presence_of :status, unless: :importing?
+    validate :valid_commit_sha, unless: :importing?
 
-    after_save :keep_around_commits
+    after_create :keep_around_commits, unless: :importing?
 
     delegate :stages, to: :statuses
 
@@ -28,45 +30,68 @@ module Ci
       end
 
       event :run do
-        transition any => :running
+        transition any - [:running] => :running
       end
 
       event :skip do
-        transition any => :skipped
+        transition any - [:skipped] => :skipped
       end
 
       event :drop do
-        transition any => :failed
+        transition any - [:failed] => :failed
       end
 
       event :succeed do
-        transition any => :success
+        transition any - [:success] => :success
       end
 
       event :cancel do
-        transition any => :canceled
+        transition any - [:canceled] => :canceled
       end
 
+      # IMPORTANT
+      # Do not add any operations to this state_machine
+      # Create a separate worker for each new operation
+
       before_transition [:created, :pending] => :running do |pipeline|
         pipeline.started_at = Time.now
       end
 
       before_transition any => [:success, :failed, :canceled] do |pipeline|
         pipeline.finished_at = Time.now
+        pipeline.update_duration
       end
 
-      before_transition do |pipeline|
-        pipeline.update_duration
+      after_transition [:created, :pending] => :running do |pipeline|
+        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
+      end
+
+      after_transition any => [:success] do |pipeline|
+        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
+      end
+
+      after_transition [:created, :pending, :running] => :success do |pipeline|
+        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
       end
 
       after_transition do |pipeline, transition|
-        pipeline.execute_hooks unless transition.loopback?
+        next if transition.loopback?
+
+        pipeline.run_after_commit do
+          PipelineHooksWorker.perform_async(id)
+        end
+      end
+
+      after_transition any => [:success, :failed] do |pipeline|
+        pipeline.run_after_commit do
+          PipelineNotificationWorker.perform_async(pipeline.id)
+        end
       end
     end
 
     # ref can't be HEAD or SHA, can only be branch/tag name
-    scope :latest_successful_for, ->(ref = default_branch) do
-      where(ref: ref).success.order(id: :desc).limit(1)
+    def self.latest_successful_for(ref)
+      where(ref: ref).order(id: :desc).success.first
     end
 
     def self.truncate_sha(sha)
@@ -78,10 +103,23 @@ module Ci
       CommitStatus.where(pipeline: pluck(:id)).stages
     end
 
+    def self.total_duration
+      where.not(duration: nil).sum(:duration)
+    end
+
+    def stages_with_latest_statuses
+      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
+    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)
+    end
+
     def valid_commit_sha
       if self.sha == Gitlab::Git::BLANK_SHA
         self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -124,7 +162,7 @@ module Ci
 
     def retryable?
       builds.latest.any? do |build|
-        build.failed? && build.retryable?
+        (build.failed? || build.canceled?) && build.retryable?
       end
     end
 
@@ -142,6 +180,10 @@ module Ci
       end
     end
 
+    def mark_as_processable_after_stage(stage_idx)
+      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+    end
+
     def latest?
       return false unless ref
       commit = project.commit(ref)
@@ -173,7 +215,7 @@ module Ci
     end
 
     def has_warnings?
-      builds.latest.ignored.any?
+      builds.latest.failed_but_allowed.any?
     end
 
     def config_processor
@@ -228,14 +270,16 @@ module Ci
       Ci::ProcessPipelineService.new(project, user).execute(self)
     end
 
-    def build_updated
-      case latest_builds_status
-      when 'pending' then enqueue
-      when 'running' then run
-      when 'success' then succeed
-      when 'failed' then drop
-      when 'canceled' then cancel
-      when 'skipped' then skip
+    def update_status
+      Gitlab::OptimisticLocking.retry_lock(self) do
+        case latest_builds_status
+        when 'pending' then enqueue
+        when 'running' then run
+        when 'success' then succeed
+        when 'failed' then drop
+        when 'canceled' then cancel
+        when 'skipped' then skip
+        end
       end
     end
 
@@ -245,8 +289,17 @@ module Ci
       ]
     end
 
+    def queued_duration
+      return unless started_at
+
+      seconds = (started_at - created_at).to_i
+      seconds unless seconds.zero?
+    end
+
     def update_duration
-      self.duration = statuses.latest.duration
+      return unless started_at
+
+      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
     end
 
     def execute_hooks
@@ -255,6 +308,14 @@ module Ci
       project.execute_services(data, :pipeline_hooks)
     end
 
+    # Merge requests for which the current pipeline is running against
+    # the merge request's latest commit.
+    def merge_requests
+      @merge_requests ||= project.merge_requests
+        .where(source_branch: self.ref)
+        .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+    end
+
     private
 
     def pipeline_data
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 49f05f881a25f246f03c5b208238da5c204f75c8..123930273e0592976fab0e4f9fd7834eac56aa7a 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,13 +2,13 @@ module Ci
   class Runner < ActiveRecord::Base
     extend Ci::Model
 
-    LAST_CONTACT_TIME = 5.minutes.ago
+    LAST_CONTACT_TIME = 1.hour.ago
     AVAILABLE_SCOPES = %w[specific shared active paused online]
     FORM_EDITABLE = %i[description tag_list active run_untagged locked]
 
-    has_many :builds, class_name: 'Ci::Build'
-    has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
-    has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
+    has_many :builds
+    has_many :runner_projects, dependent: :destroy
+    has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
 
     has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
 
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 4b44ffa886e6b838853d1cffea9660de1f3595e5..1f9baeca5b17250bc4f89cb343daa5e0b3133ea9 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,8 +2,8 @@ module Ci
   class RunnerProject < ActiveRecord::Base
     extend Ci::Model
     
-    belongs_to :runner, class_name: 'Ci::Runner'
-    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+    belongs_to :runner
+    belongs_to :project, foreign_key: :gl_project_id
 
     validates_uniqueness_of :runner_id, scope: :gl_project_id
   end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index a0b19b51a127a44372149276097c2a8ac6cdd5f7..62889fe80d8c4045d38d55046b6733f057774fc0 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,8 +4,8 @@ module Ci
 
     acts_as_paranoid
 
-    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
-    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+    belongs_to :project, foreign_key: :gl_project_id
+    has_many :trigger_requests, dependent: :destroy
 
     validates_presence_of :token
     validates_uniqueness_of :token
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index fc674871743bdb414da97fe199a5fd75a03d1ed1..2b807731d0d73a54b57bdb7b8dbd1803a98183d2 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -2,9 +2,9 @@ module Ci
   class TriggerRequest < ActiveRecord::Base
     extend Ci::Model
 
-    belongs_to :trigger, class_name: 'Ci::Trigger'
-    belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
-    has_many :builds, class_name: 'Ci::Build'
+    belongs_to :trigger
+    belongs_to :pipeline, foreign_key: :commit_id
+    has_many :builds
 
     serialize :variables
 
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index c9c47ec7419641462301456fe88268bd77ed57ba..94d9e2b3208d040510262f7965bded9849b764d7 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,8 +1,8 @@
 module Ci
   class Variable < ActiveRecord::Base
     extend Ci::Model
-    
-    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+
+    belongs_to :project, foreign_key: :gl_project_id
 
     validates_uniqueness_of :key, scope: :gl_project_id
     validates :key,
@@ -11,7 +11,9 @@ module Ci
       format: { with: /\A[a-zA-Z0-9_]+\z/,
                 message: "can contain only letters, digits and '_'." }
 
-    attr_encrypted :value, 
+    scope :order_key_asc, -> { reorder(key: :asc) }
+
+    attr_encrypted :value,
        mode: :per_attribute_iv_and_salt,
        insecure_mode: true,
        key: Gitlab::Application.secrets.db_key_base,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cc413448ce891615492a2546d2967e8aa948da60..9e7fde9503d0fc0ade1b1bb52e6508f6114f3eb6 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -108,15 +108,6 @@ class Commit
     @diff_line_count
   end
 
-  # Returns a string describing the commit for use in a link title
-  #
-  # Example
-  #
-  #   "Commit: Alex Denisov - Project git clone panel"
-  def link_title
-    "Commit: #{author_name} - #{title}"
-  end
-
   # Returns the commits title.
   #
   # Usually, the commit title is the first line of the commit message.
@@ -229,18 +220,25 @@ class Commit
 
   def diff_refs
     Gitlab::Diff::DiffRefs.new(
-      base_sha: self.parent_id || self.sha,
+      base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
       head_sha: self.sha
     )
   end
 
   def pipelines
-    @pipeline ||= project.pipelines.where(sha: sha)
+    project.pipelines.where(sha: sha)
   end
 
-  def status
-    return @status if defined?(@status)
-    @status ||= pipelines.status
+  def status(ref = nil)
+    @statuses ||= {}
+
+    if @statuses.key?(ref)
+      @statuses[ref]
+    elsif ref
+      @statuses[ref] = pipelines.where(ref: ref).status
+    else
+      @statuses[ref] = pipelines.status
+    end
   end
 
   def revert_branch_name
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 630ee9601e0212e418e6363b6f7d0c0694b137cc..ac2477fd9734cfa86505992051fec5998e478e72 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -4,12 +4,10 @@
 #
 #   range = CommitRange.new('f3f85602...e86e1013', project)
 #   range.exclude_start?  # => false
-#   range.reference_title # => "Commits f3f85602 through e86e1013"
 #   range.to_s            # => "f3f85602...e86e1013"
 #
 #   range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
 #   range.exclude_start?  # => true
-#   range.reference_title # => "Commits f3f85602^ through e86e1013"
 #   range.to_param        # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
 #   range.to_s            # => "f3f85602..e86e1013"
 #
@@ -82,7 +80,7 @@ class CommitRange
   end
 
   def inspect
-    %(#<#{self.class}:#{object_id} #{to_s}>)
+    %(#<#{self.class}:#{object_id} #{self}>)
   end
 
   def to_s
@@ -109,11 +107,6 @@ class CommitRange
     reference
   end
 
-  # Returns a String for use in a link's title attribute
-  def reference_title
-    "Commits #{sha_start} through #{sha_to}"
-  end
-
   # Return a Hash of parameters for passing to a URL helper
   #
   # See `namespace_project_compare_url`
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 703ca90edb6982bee860ca1825bb55d908eaf170..d159fc6c5c78ff77d7481add3a25d45f68b2ad84 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,10 +1,11 @@
 class CommitStatus < ActiveRecord::Base
-  include Statuseable
+  include HasStatus
   include Importable
+  include AfterCommitQueue
 
   self.table_name = 'ci_builds'
 
-  belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+  belongs_to :project, foreign_key: :gl_project_id
   belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :user
 
@@ -21,15 +22,37 @@ class CommitStatus < ActiveRecord::Base
 
     where(id: max_id.group(:name, :commit_id))
   end
+
   scope :retried, -> { where.not(id: latest) }
   scope :ordered, -> { order(:name) }
-  scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+
+  scope :failed_but_allowed, -> do
+    where(allow_failure: true, status: [:failed, :canceled])
+  end
+
+  scope :exclude_ignored, -> do
+    quoted_when = connection.quote_column_name('when')
+    # We want to ignore failed_but_allowed jobs
+    where("allow_failure = ? OR status IN (?)",
+      false, all_state_names - [:failed, :canceled]).
+      # We want to ignore skipped manual jobs
+      where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
+      # We want to ignore skipped on_failure
+      where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+  end
+
+  scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
+  scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
 
   state_machine :status do
     event :enqueue do
       transition [:created, :skipped] => :pending
     end
 
+    event :process do
+      transition skipped: :created
+    end
+
     event :run do
       transition pending: :running
     end
@@ -50,35 +73,37 @@ class CommitStatus < ActiveRecord::Base
       transition [:created, :pending, :running] => :canceled
     end
 
-    after_transition created: [:pending, :running] do |commit_status|
-      commit_status.update_attributes queued_at: Time.now
+    before_transition created: [:pending, :running] do |commit_status|
+      commit_status.queued_at = Time.now
     end
 
-    after_transition [:created, :pending] => :running do |commit_status|
-      commit_status.update_attributes started_at: Time.now
+    before_transition [:created, :pending] => :running do |commit_status|
+      commit_status.started_at = Time.now
     end
 
-    after_transition any => [:success, :failed, :canceled] do |commit_status|
-      commit_status.update_attributes finished_at: Time.now
-    end
-
-    # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
-    around_transition any => [:success, :failed, :canceled] do |commit_status, block|
-      block.call
-
-      commit_status.pipeline.try(:process!)
+    before_transition any => [:success, :failed, :canceled] do |commit_status|
+      commit_status.finished_at = Time.now
     end
 
     after_transition do |commit_status, transition|
-      commit_status.pipeline.try(:build_updated) unless transition.loopback?
-    end
-
-    after_transition [:created, :pending, :running] => :success do |commit_status|
-      MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
+      next if transition.loopback?
+
+      commit_status.run_after_commit do
+        pipeline.try do |pipeline|
+          if complete?
+            PipelineProcessWorker.perform_async(pipeline.id)
+          else
+            PipelineUpdateWorker.perform_async(pipeline.id)
+          end
+        end
+      end
     end
 
     after_transition any => :failed do |commit_status|
-      MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
+      commit_status.run_after_commit do
+        MergeRequests::AddTodoWhenBuildFailsService
+          .new(pipeline.project, nil).execute(self)
+      end
     end
   end
 
@@ -88,6 +113,10 @@ class CommitStatus < ActiveRecord::Base
     pipeline.before_sha || Gitlab::Git::BLANK_SHA
   end
 
+  def group_name
+    name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+  end
+
   def self.stages
     # We group by stage name, but order stages by theirs' index
     unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
@@ -102,18 +131,16 @@ class CommitStatus < ActiveRecord::Base
     end
   end
 
-  def ignored?
+  def failed_but_allowed?
     allow_failure? && (failed? || canceled?)
   end
 
+  def playable?
+    false
+  end
+
   def duration
-    duration =
-      if started_at && finished_at
-        finished_at - started_at
-      elsif started_at
-        Time.now - started_at
-      end
-    duration
+    calculate_duration
   end
 
   def stuck?
diff --git a/app/models/compare.rb b/app/models/compare.rb
index 4856510f5263360306304d6b1be45fdab2db6297..3a8bbcb1acdf2ac8b24edd9e3dc915c5fe1c0668 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -11,9 +11,10 @@ class Compare
     end
   end
 
-  def initialize(compare, project)
+  def initialize(compare, project, straight: false)
     @compare = compare
     @project = project
+    @straight = straight
   end
 
   def commits
@@ -45,6 +46,18 @@ class Compare
                    end
   end
 
+  def start_commit_sha
+    start_commit.try(:sha)
+  end
+
+  def base_commit_sha
+    base_commit.try(:sha)
+  end
+
+  def head_commit_sha
+    commit.try(:sha)
+  end
+
   def raw_diffs(*args)
     @compare.diffs(*args)
   end
@@ -58,9 +71,9 @@ class Compare
 
   def diff_refs
     Gitlab::Diff::DiffRefs.new(
-      base_sha:  base_commit.try(:sha),
-      start_sha: start_commit.try(:sha),
-      head_sha: commit.try(:sha)
+      base_sha:  @straight ? start_commit_sha : base_commit_sha,
+      start_sha: start_commit_sha,
+      head_sha: head_commit_sha
     )
   end
 end
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
index eedd32a729fb660a8825cd7d5103ca2589e0ca58..62bc6b809f405bff4ec3035bc5e776c40656416b 100644
--- a/app/models/concerns/access_requestable.rb
+++ b/app/models/concerns/access_requestable.rb
@@ -8,9 +8,6 @@ module AccessRequestable
   extend ActiveSupport::Concern
 
   def request_access(user)
-    members.create(
-      access_level: Gitlab::Access::DEVELOPER,
-      user: user,
-      requested_at: Time.now.utc)
+    Members::RequestAccessService.new(self, user).execute
   end
 end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 800a16ab246c83db03d993b6b45154bab80ab987..073ac4c1b65ef43ae2fb8ad8711aad8f629b87c9 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -2,7 +2,7 @@ module Awardable
   extend ActiveSupport::Concern
 
   included do
-    has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy
+    has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy
 
     if self < Participable
       # By default we always load award_emoji user association
@@ -59,6 +59,24 @@ module Awardable
     true
   end
 
+  def awardable_votes?(name)
+    AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
+  end
+
+  def user_can_award?(current_user, name)
+    if user_authored?(current_user)
+      !awardable_votes?(normalize_name(name))
+    else
+      true
+    end
+  end
+
+  def user_authored?(current_user)
+    author = self.respond_to?(:author) ? self.author : self.user
+
+    author == current_user
+  end
+
   def awarded_emoji?(emoji_name, current_user)
     award_emoji.where(name: emoji_name, user: current_user).exists?
   end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
new file mode 100644
index 0000000000000000000000000000000000000000..90bd6490a02e901b2fc4816756217b81ea328d98
--- /dev/null
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -0,0 +1,131 @@
+# This module takes care of updating cache columns for Markdown-containing
+# fields. Use like this in the body of your class:
+#
+#     include CacheMarkdownField
+#     cache_markdown_field :foo
+#     cache_markdown_field :bar
+#     cache_markdown_field :baz, pipeline: :single_line
+#
+# Corresponding foo_html, bar_html and baz_html fields should exist.
+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
+
+    def html_field(markdown_field)
+      "#{markdown_field}_html"
+    end
+
+    def html_fields
+      markdown_fields.map {|field| html_field(field) }
+    end
+  end
+
+  # Dynamic registries don't really work in Rails as it's not guaranteed that
+  # every class will be loaded, so hardcode the list.
+  CACHING_CLASSES = %w[
+    AbuseReport
+    Appearance
+    ApplicationSetting
+    BroadcastMessage
+    Issue
+    Label
+    MergeRequest
+    Milestone
+    Namespace
+    Note
+    Project
+    Release
+    Snippet
+  ]
+
+  def self.caching_classes
+    CACHING_CLASSES.map(&:constantize)
+  end
+
+  extend ActiveSupport::Concern
+
+  included do
+    cattr_reader :cached_markdown_fields do
+      FieldData.new
+    end
+
+    # Returns the default Banzai render context for the cached markdown field.
+    def banzai_render_context(field)
+      raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+        cached_markdown_fields.markdown_fields.include?(field)
+
+      # Always include a project key, or Banzai complains
+      project = self.project if self.respond_to?(:project)
+      context = cached_markdown_fields[field].merge(project: project)
+
+      # Banzai is less strict about authors, so don't always have an author key
+      context[:author] = self.author if self.respond_to?(:author)
+
+      context
+    end
+
+    # Allow callers to look up the cache field name, rather than hardcoding it
+    def markdown_cache_field_for(field)
+      raise ArgumentError.new("Unknown field: #{field}") unless
+        cached_markdown_fields.markdown_fields.include?(field)
+
+      cached_markdown_fields.html_field(field)
+    end
+
+    # Always exclude _html fields from attributes (including serialization).
+    # They contain unredacted HTML, which would be a security issue
+    alias_method :attributes_before_markdown_cache, :attributes
+    def attributes
+      attrs = attributes_before_markdown_cache
+
+      cached_markdown_fields.html_fields.each do |field|
+        attrs.delete(field)
+      end
+
+      attrs
+    end
+  end
+
+  class_methods do
+    private
+
+    # Specify that a field is markdown. Its rendered output will be cached in
+    # a corresponding _html field. Any custom rendering options may be provided
+    # as a context.
+    def cache_markdown_field(markdown_field, context = {})
+      raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
+        CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
+
+      cached_markdown_fields[markdown_field] = context
+
+      html_field = cached_markdown_fields.html_field(markdown_field)
+      cache_method = "#{markdown_field}_cache_refresh".to_sym
+      invalidation_method = "#{html_field}_invalidated?".to_sym
+
+      define_method(cache_method) do
+        html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+        __send__("#{html_field}=", html)
+        true
+      end
+
+      # The HTML becomes invalid if any dependent fields change. For now, assume
+      # author and project invalidate the cache in all circumstances.
+      define_method(invalidation_method) do
+        changed_fields = changed_attributes.keys
+        invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
+        !invalidations.empty?
+      end
+
+      before_save cache_method, if: invalidation_method
+    end
+  end
+end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b66ba08dc593945b320c7f9a7210f66d618c91f5
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,19 @@
+module Expirable
+  extend ActiveSupport::Concern
+
+  included do
+    scope :expired, -> { where('expires_at <= ?', Time.current) }
+  end
+
+  def expired?
+    expires? && expires_at <= Time.current
+  end
+
+  def expires?
+    expires_at.present?
+  end
+
+  def expires_soon?
+    expires? && expires_at < 7.days.from_now
+  end
+end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/has_status.rb
similarity index 67%
rename from app/models/concerns/statuseable.rb
rename to app/models/concerns/has_status.rb
index 5d4b0a868998c82032f261ea6982cf665b9fef1e..ef3e73a4072d0cdc3ed0b77e16709d8d35a45055 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,43 +1,40 @@
-module Statuseable
+module HasStatus
   extend ActiveSupport::Concern
 
   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]
+  ORDERED_STATUSES = %w[failed pending running canceled success skipped]
 
   class_methods do
     def status_sql
-      scope = all.relevant
+      scope = if respond_to?(:exclude_ignored)
+                exclude_ignored
+              else
+                all
+              end
       builds = scope.select('count(*)').to_sql
+      created = scope.created.select('count(*)').to_sql
       success = scope.success.select('count(*)').to_sql
-      ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
-      ignored ||= '0'
       pending = scope.pending.select('count(*)').to_sql
       running = scope.running.select('count(*)').to_sql
-      canceled = scope.canceled.select('count(*)').to_sql
       skipped = scope.skipped.select('count(*)').to_sql
+      canceled = scope.canceled.select('count(*)').to_sql
 
-      deduce_status = "(CASE
-        WHEN (#{builds})=0 THEN NULL
-        WHEN (#{builds})=(#{skipped}) THEN 'skipped'
-        WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
-        WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
-        WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
-        WHEN (#{running})+(#{pending})>0 THEN 'running'
+      "(CASE
+        WHEN (#{builds})=(#{success}) THEN 'success'
+        WHEN (#{builds})=(#{created}) THEN 'created'
+        WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+        WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
+        WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
+        WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
         ELSE 'failed'
       END)"
-
-      deduce_status
     end
 
     def status
-      all.pluck(self.status_sql).first
-    end
-
-    def duration
-      duration_array = all.map(&:duration).compact
-      duration_array.reduce(:+)
+      all.pluck(status_sql).first
     end
 
     def started_at
@@ -47,6 +44,10 @@ module Statuseable
     def finished_at
       all.maximum(:finished_at)
     end
+
+    def all_state_names
+      state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
+    end
   end
 
   included do
@@ -85,4 +86,14 @@ module Statuseable
   def complete?
     COMPLETED_STATUSES.include?(status)
   end
+
+  private
+
+  def calculate_duration
+    if started_at && finished_at
+      finished_at - started_at
+    elsif started_at
+      Time.now - started_at
+    end
+  end
 end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cbae1cd439bc3d0ffdd0757679570e35ee62c3ef..664bb594aa9f1a96c6ab32517a671894a4ebbd67 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,13 +6,18 @@
 #
 module Issuable
   extend ActiveSupport::Concern
+  include CacheMarkdownField
   include Participable
   include Mentionable
   include Subscribable
   include StripAttribute
   include Awardable
+  include Taskable
 
   included do
+    cache_markdown_field :title, pipeline: :single_line
+    cache_markdown_field :description
+
     belongs_to :author, class_name: "User"
     belongs_to :assignee, class_name: "User"
     belongs_to :updated_by, class_name: "User"
@@ -28,10 +33,13 @@ module Issuable
         loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
       end
     end
+
     has_many :label_links, as: :target, dependent: :destroy
     has_many :labels, through: :label_links
     has_many :todos, as: :target, dependent: :destroy
 
+    has_one :metrics
+
     validates :author, presence: true
     validates :title, presence: true, length: { within: 0..255 }
 
@@ -81,12 +89,19 @@ module Issuable
     acts_as_paranoid
 
     after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+    after_save :record_metrics
 
     def update_assignee_cache_counts
       # make sure we flush the cache for both the old *and* new assignee
       User.find(assignee_id_was).update_cache_counts if assignee_id_was
       assignee.update_cache_counts if assignee
     end
+
+    # We want to use optimistic lock for cases when only title or description are involved
+    # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
+    def locking_enabled?
+      title_changed? || description_changed?
+    end
   end
 
   module ClassMethods
@@ -131,7 +146,16 @@ module Issuable
     end
 
     def order_labels_priority(excluded_labels: [])
-      select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+      params = {
+        target_type: name,
+        target_column: "#{table_name}.id",
+        project_column: "#{table_name}.#{project_foreign_key}",
+        excluded_labels: excluded_labels
+      }
+
+      highest_priority = highest_label_priority(params).to_sql
+
+      select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
         group(arel_table[:id]).
         reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
     end
@@ -160,18 +184,8 @@ module Issuable
       grouping_columns
     end
 
-    private
-
-    def highest_label_priority(excluded_labels)
-      query = Label.select(Label.arel_table[:priority].minimum).
-        joins(:label_links).
-        where(label_links: { target_type: name }).
-        where("label_links.target_id = #{table_name}.id").
-        reorder(nil)
-
-      query.where.not(title: excluded_labels) if excluded_labels.present?
-
-      query
+    def to_ability_name
+      model_name.singular
     end
   end
 
@@ -227,18 +241,6 @@ module Issuable
     labels.order('title ASC').pluck(:title)
   end
 
-  def remove_labels
-    labels.delete_all
-  end
-
-  def add_labels_by_names(label_names)
-    label_names.each do |label_name|
-      label = project.labels.create_with(color: Label::DEFAULT_COLOR).
-        find_or_create_by(title: label_name.strip)
-      self.labels << label
-    end
-  end
-
   # Convert this Issuable class name to a format usable by Ability definitions
   #
   # Examples:
@@ -246,7 +248,7 @@ module Issuable
   #   issuable.class           # => MergeRequest
   #   issuable.to_ability_name # => "merge_request"
   def to_ability_name
-    self.class.to_s.underscore
+    self.class.to_ability_name
   end
 
   # Returns a Hash of attributes to be used for Twitter card metadata
@@ -287,4 +289,14 @@ module Issuable
   def can_move?(*)
     false
   end
+
+  def assignee_or_author?(user)
+    # We're comparing IDs here so we don't need to load any associations.
+    author_id == user.id || assignee_id == user.id
+  end
+
+  def record_metrics
+    metrics = self.metrics || create_metrics
+    metrics.record!
+  end
 end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index ec9e0f1b1d0a6537b45bd9131577204198abf8da..eb2ff0428f6656a5170eb1e7e7bdd472a0d837f1 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -43,19 +43,15 @@ module Mentionable
     self
   end
 
-  def all_references(current_user = nil, text = nil, extractor: nil)
+  def all_references(current_user = nil, extractor: nil)
     extractor ||= Gitlab::ReferenceExtractor.
       new(project, current_user)
 
-    if text
-      extractor.analyze(text, author: author)
-    else
-      self.class.mentionable_attrs.each do |attr, options|
-        text = __send__(attr)
-        options = options.merge(cache_key: [self, attr], author: author)
+    self.class.mentionable_attrs.each do |attr, options|
+      text    = __send__(attr)
+      options = options.merge(cache_key: [self, attr], author: author)
 
-        extractor.analyze(text, options)
-      end
+      extractor.analyze(text, options)
     end
 
     extractor
@@ -66,8 +62,8 @@ module Mentionable
   end
 
   # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
-  def referenced_mentionables(current_user = self.author, text = nil)
-    refs = all_references(current_user, text)
+  def referenced_mentionables(current_user = self.author)
+    refs = all_references(current_user)
     refs = (refs.issues + refs.merge_requests + refs.commits)
 
     # We're using this method instead of Array diffing because that requires
@@ -77,8 +73,8 @@ module Mentionable
   end
 
   # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
-  def create_cross_references!(author = self.author, without = [], text = nil)
-    refs = referenced_mentionables(author, text)
+  def create_cross_references!(author = self.author, without = [])
+    refs = referenced_mentionables(author)
 
     # We're using this method instead of Array diffing because that requires
     # both of the object's `hash` values to be the same, which may not be the
@@ -97,10 +93,7 @@ module Mentionable
 
     return if changes.empty?
 
-    original_text = changes.collect { |_, vals| vals.first }.join(' ')
-
-    preexisting = referenced_mentionables(author, original_text)
-    create_cross_references!(author, preexisting)
+    create_cross_references!(author)
   end
 
   private
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 4be6a2f621b322b5c2b9d582d757454b1108d19a..b8dd27a7afe5d0bfffeb50d7bbac37613733339a 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -17,6 +17,10 @@ module NoteOnDiff
     raise NotImplementedError
   end
 
+  def original_line_code
+    raise NotImplementedError
+  end
+
   def diff_attributes
     raise NotImplementedError
   end
@@ -24,4 +28,8 @@ module NoteOnDiff
   def can_be_award_emoji?
     false
   end
+
+  def to_discussion
+    Discussion.new([self])
+  end
 end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d88951c7135bd3c9322dba937272ed9626c7388
--- /dev/null
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -0,0 +1,37 @@
+# Makes api V3 compatible with old project features permissions methods
+#
+# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
+# fields to a new table "project_features", support for the old fields is still needed in the API.
+
+module ProjectFeaturesCompatibility
+  extend ActiveSupport::Concern
+
+  def wiki_enabled=(value)
+    write_feature_attribute(:wiki_access_level, value)
+  end
+
+  def builds_enabled=(value)
+    write_feature_attribute(:builds_access_level, value)
+  end
+
+  def merge_requests_enabled=(value)
+    write_feature_attribute(:merge_requests_access_level, value)
+  end
+
+  def issues_enabled=(value)
+    write_feature_attribute(:issues_access_level, value)
+  end
+
+  def snippets_enabled=(value)
+    write_feature_attribute(:snippets_access_level, value)
+  end
+
+  private
+
+  def write_feature_attribute(field, value)
+    build_project_feature unless project_feature
+
+    access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+    project_feature.update_attribute(field, access_level)
+  end
+end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 5a7b36070e79e8fe43c3bcddb3ac12cd27efbe81..7fd0905ee818d4e41109b006864d9f0af6680c3f 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -1,6 +1,11 @@
 module ProtectedBranchAccess
   extend ActiveSupport::Concern
 
+  included do
+    scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
+    scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
+  end
+
   def humanize
     self.class.human_access_levels[self.access_level]
   end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd2879d2b67275b90e8e3c78c81849a..7edb0acd56c75d9f48187e3aa6ee2339b835c3a3 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,26 @@ module Sortable
         all
       end
     end
+
+    private
+
+    def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
+      query = Label.select(LabelPriority.arel_table[:priority].minimum).
+        left_join_priorities.
+        joins(:label_links).
+        where("label_priorities.project_id = #{project_column}").
+        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 = query.where.not(title: excluded_labels) if excluded_labels.present?
+
+      query
+    end
   end
 end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index ce54fe5d3bf8d606223121e3de5088b711e237df..1aa97debe426fcfe3c893603833e6119da366191 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -23,7 +23,7 @@ module Spammable
 
   def submittable_as_spam?
     if user_agent_detail
-      user_agent_detail.submittable?
+      user_agent_detail.submittable? && current_application_settings.akismet_enabled
     else
       false
     end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index df2a9e3e84be06a8d7c8a8f01b3de00cb5382d4b..ebc75100a546c8e298fb8c206cceca7c9390b562 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -52,11 +52,23 @@ module Taskable
   end
 
   # Return a string that describes the current state of this Taskable's task
-  # list items, e.g. "20 tasks (12 completed, 8 remaining)"
-  def task_status
+  # list items, e.g. "12 of 20 tasks completed"
+  def task_status(short: false)
     return '' if description.blank?
 
+    prep, completed = if short
+                        ['/', '']
+                      else
+                        [' of ', ' completed']
+                      end
+
     sum = tasks.summary
-    "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
+    "#{sum.complete_count}#{prep}#{sum.item_count} #{'task'.pluralize(sum.item_count)}#{completed}"
+  end
+
+  # Return a short string that describes the current state of this Taskable's
+  # task list items -- for small screens
+  def task_status_short
+    task_status(short: true)
   end
 end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 24c7b26d223d2536e1ca773f48208f03ca0ca1f9..04d30f462101b4428c1006db830265a5661bad2a 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -4,17 +4,21 @@ module TokenAuthenticatable
   private
 
   def write_new_token(token_field)
-    new_token = generate_token(token_field)
+    new_token = generate_available_token(token_field)
     write_attribute(token_field, new_token)
   end
 
-  def generate_token(token_field)
+  def generate_available_token(token_field)
     loop do
-      token = Devise.friendly_token
+      token = generate_token(token_field)
       break token unless self.class.unscoped.find_by(token_field => token)
     end
   end
 
+  def generate_token(token_field)
+    Devise.friendly_token
+  end
+
   class_methods do
     def authentication_token_fields
       @token_fields || []
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8ed4a56b19b86443aa08981f51d80511f1f1b5df
--- /dev/null
+++ b/app/models/cycle_analytics.rb
@@ -0,0 +1,103 @@
+class CycleAnalytics
+  include Gitlab::Database::Median
+  include Gitlab::Database::DateTime
+
+  DEPLOYMENT_METRIC_STAGES = %i[production staging]
+
+  def initialize(project, from:)
+    @project = project
+    @from = from
+  end
+
+  def summary
+    @summary ||= Summary.new(@project, from: @from)
+  end
+
+  def issue
+    calculate_metric(:issue,
+                     Issue.arel_table[:created_at],
+                     [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+                      Issue::Metrics.arel_table[:first_added_to_board_at]])
+  end
+
+  def plan
+    calculate_metric(:plan,
+                     [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+                      Issue::Metrics.arel_table[:first_added_to_board_at]],
+                     Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
+  end
+
+  def code
+    calculate_metric(:code,
+                     Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
+                     MergeRequest.arel_table[:created_at])
+  end
+
+  def test
+    calculate_metric(:test,
+                     MergeRequest::Metrics.arel_table[:latest_build_started_at],
+                     MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+  end
+
+  def review
+    calculate_metric(:review,
+                     MergeRequest.arel_table[:created_at],
+                     MergeRequest::Metrics.arel_table[:merged_at])
+  end
+
+  def staging
+    calculate_metric(:staging,
+                     MergeRequest::Metrics.arel_table[:merged_at],
+                     MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+  end
+
+  def production
+    calculate_metric(:production,
+                     Issue.arel_table[:created_at],
+                     MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+  end
+
+  private
+
+  def calculate_metric(name, start_time_attrs, end_time_attrs)
+    cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+    # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+    # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+    # We compute the (end_time - start_time) interval, and give it an alias based on the current
+    # cycle analytics stage.
+    interval_query = Arel::Nodes::As.new(
+      cte_table,
+      subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
+
+    median_datetime(cte_table, interval_query, name)
+  end
+
+  # Join table with a row for every <issue,merge_request> pair (where the merge request
+  # closes the given issue) with issue and merge request metrics included. The metrics
+  # are loaded with an inner join, so issues / merge requests without metrics are
+  # automatically excluded.
+  def base_query_for(name)
+    arel_table = MergeRequestsClosingIssues.arel_table
+
+    # Load issues
+    query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
+            join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
+            where(Issue.arel_table[:project_id].eq(@project.id)).
+            where(Issue.arel_table[:deleted_at].eq(nil)).
+            where(Issue.arel_table[:created_at].gteq(@from))
+
+    # Load merge_requests
+    query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
+            on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
+            join(MergeRequest::Metrics.arel_table).
+            on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
+
+    if DEPLOYMENT_METRIC_STAGES.include?(name)
+      # Limit to merge requests that have been deployed to production after `@from`
+      query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+    end
+
+    query
+  end
+end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b46db449bf325d80688d7abe77f7515d1af640b3
--- /dev/null
+++ b/app/models/cycle_analytics/summary.rb
@@ -0,0 +1,42 @@
+class CycleAnalytics
+  class Summary
+    def initialize(project, from:)
+      @project = project
+      @from = from
+    end
+
+    def new_issues
+      @project.issues.created_after(@from).count
+    end
+
+    def commits
+      ref = @project.default_branch.presence
+      count_commits_for(ref)
+    end
+
+    def deploys
+      @project.deployments.where("created_at > ?", @from).count
+    end
+
+    private
+
+    # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+    # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+    # the easiest way forward is to replicate the relevant portions of the
+    # `log` function here.
+    def count_commits_for(ref)
+      return unless ref
+
+      repository = @project.repository.raw_repository
+      sha = @project.repository.commit(ref).sha
+
+      cmd = %W(git --git-dir=#{repository.path} log)
+      cmd << '--format=%H'
+      cmd << "--after=#{@from.iso8601}"
+      cmd << sha
+
+      raw_output = IO.popen(cmd) { |io| io.read }
+      raw_output.lines.count
+    end
+  end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e338889714ccd5b69980fc5ad57a725258a0b5c..91d85c2279bf2da830caaf083315d7232a99ef7c 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
 
   delegate :name, to: :environment, prefix: true
 
-  after_save :keep_around_commit
+  after_create :create_ref
 
   def commit
     project.commit(sha)
@@ -29,17 +29,79 @@ class Deployment < ActiveRecord::Base
     self == environment.last_deployment
   end
 
-  def keep_around_commit
-    project.repository.keep_around(self.sha)
+  def create_ref
+    project.repository.create_ref(ref, ref_path)
   end
 
   def manual_actions
-    deployable.try(:other_actions)
+    @manual_actions ||= deployable.try(:other_actions)
   end
 
   def includes_commit?(commit)
     return false unless commit
 
-    project.repository.is_ancestor?(commit.id, sha)
+    # Before 8.10, deployments didn't have keep-around refs. Any deployment
+    # created before then could have a `sha` referring to a commit that no
+    # longer exists in the repository, so just ignore those.
+    begin
+      project.repository.is_ancestor?(commit.id, sha)
+    rescue Rugged::OdbError
+      false
+    end
+  end
+
+  def update_merge_request_metrics!
+    return unless environment.update_merge_request_metrics?
+
+    merge_requests = project.merge_requests.
+                     joins(:metrics).
+                     where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }).
+                     where("merge_request_metrics.merged_at <= ?", self.created_at)
+
+    if previous_deployment
+      merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+    end
+
+    # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
+    # that we're updating.
+    merge_request_ids =
+      if Gitlab::Database.postgresql?
+        merge_requests.select(:id)
+      elsif Gitlab::Database.mysql?
+        merge_requests.map(&:id)
+      end
+
+    MergeRequest::Metrics.
+      where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil).
+      update_all(first_deployed_to_production_at: self.created_at)
+  end
+
+  def previous_deployment
+    @previous_deployment ||=
+      project.deployments.joins(:environment).
+      where(environments: { name: self.environment.name }, ref: self.ref).
+      where.not(id: self.id).
+      take
+  end
+
+  def stop_action
+    return nil unless on_stop.present?
+    return nil unless manual_actions
+
+    @stop_action ||= manual_actions.find_by(name: on_stop)
+  end
+
+  def stoppable?
+    stop_action.present?
+  end
+
+  def formatted_deployment_time
+    created_at.to_time.in_time_zone.to_s(:medium)
+  end
+
+  private
+
+  def ref_path
+    File.join(environment.ref_path, 'deployments', iid.to_s)
   end
 end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c816deb4e0cb65d2b4f2a19c24dfa78f55381856..559b30759050c58305cedbba8ace0df22e78fa51 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,17 +9,37 @@ class DiffNote < Note
   validates :diff_line, presence: true
   validates :line_code, presence: true, line_code: true
   validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+  validates :resolved_by, presence: true, if: :resolved?
   validate :positions_complete
   validate :verify_supported
 
+  # Keep this scope in sync with the logic in `#resolvable?`
+  scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
+  scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
+  scope :unresolved, -> { resolvable.where(resolved_at: nil) }
+
+  after_initialize :ensure_original_discussion_id
   before_validation :set_original_position, :update_position, on: :create
-  before_validation :set_line_code
+  before_validation :set_line_code, :set_original_discussion_id
+  # We need to do this again, because it's already in `Note`, but is affected by
+  # `update_position` and needs to run after that.
+  before_validation :set_discussion_id
   after_save :keep_around_commits
 
   class << self
     def build_discussion_id(noteable_type, noteable_id, position)
       [super(noteable_type, noteable_id), *position.key].join("-")
     end
+
+    # This method must be kept in sync with `#resolve!`
+    def resolve!(current_user)
+      unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+    end
+
+    # This method must be kept in sync with `#unresolve!`
+    def unresolve!
+      resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+    end
   end
 
   def new_diff_note?
@@ -30,14 +50,6 @@ class DiffNote < Note
     { position: position.to_json }
   end
 
-  def discussion_id
-    @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
-  end
-
-  def original_discussion_id
-    @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
-  end
-
   def position=(new_position)
     if new_position.is_a?(String)
       new_position = JSON.parse(new_position) rescue nil
@@ -63,6 +75,10 @@ class DiffNote < Note
     diff_file.position(line) == self.original_position
   end
 
+  def original_line_code
+    self.diff_file.line_code(self.diff_line)
+  end
+
   def active?(diff_refs = nil)
     return false unless supported?
     return true if for_commit?
@@ -72,10 +88,47 @@ class DiffNote < Note
     self.position.diff_refs == diff_refs
   end
 
+  # If you update this method remember to also update the scope `resolvable`
+  def resolvable?
+    !system? && for_merge_request?
+  end
+
+  def resolved?
+    return false unless resolvable?
+
+    self.resolved_at.present?
+  end
+
+  # If you update this method remember to also update `.resolve!`
+  def resolve!(current_user)
+    return unless resolvable?
+    return if resolved?
+
+    self.resolved_at = Time.now
+    self.resolved_by = current_user
+    save!
+  end
+
+  # If you update this method remember to also update `.unresolve!`
+  def unresolve!
+    return unless resolvable?
+    return unless resolved?
+
+    self.resolved_at = nil
+    self.resolved_by = nil
+    save!
+  end
+
+  def discussion
+    return unless resolvable?
+
+    self.noteable.find_diff_discussion(self.discussion_id)
+  end
+
   private
 
   def supported?
-    !self.for_merge_request? || self.noteable.support_new_diff_notes?
+    for_commit? || self.noteable.has_complete_diff_refs?
   end
 
   def noteable_diff_refs
@@ -94,6 +147,26 @@ class DiffNote < Note
     self.line_code = self.position.line_code(self.project.repository)
   end
 
+  def ensure_original_discussion_id
+    return unless self.persisted?
+    return if self.original_discussion_id
+
+    set_original_discussion_id
+    update_column(:original_discussion_id, self.original_discussion_id)
+  end
+
+  def set_original_discussion_id
+    self.original_discussion_id = Digest::SHA1.hexdigest(build_original_discussion_id)
+  end
+
+  def build_discussion_id
+    self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
+  end
+
+  def build_original_discussion_id
+    self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
+  end
+
   def update_position
     return unless supported?
     return if for_commit?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index e2218a5f02bed4b40bfbd510b85310b4a6f4b5b6..de06c13481a1899aa80110139bbd6b700f6091b7 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -1,7 +1,7 @@
 class Discussion
   NUMBER_OF_TRUNCATED_DIFF_LINES = 16
 
-  attr_reader :first_note, :notes
+  attr_reader :notes
 
   delegate  :created_at,
             :project,
@@ -12,12 +12,19 @@ class Discussion
             :for_merge_request?,
 
             :line_code,
+            :original_line_code,
             :diff_file,
             :for_line?,
             :active?,
 
             to: :first_note
 
+  delegate  :resolved_at,
+            :resolved_by,
+
+            to: :last_resolved_note,
+            allow_nil: true
+
   delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
 
   def self.for_notes(notes)
@@ -29,14 +36,29 @@ class Discussion
   end
 
   def initialize(notes)
-    @first_note = notes.first
     @notes = notes
   end
 
+  def last_resolved_note
+    return unless resolved?
+
+    @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+  end
+
+  def last_updated_at
+    last_note.created_at
+  end
+
+  def last_updated_by
+    last_note.author
+  end
+
   def id
     first_note.discussion_id
   end
 
+  alias_method :to_param, :id
+
   def diff_discussion?
     first_note.diff_note?
   end
@@ -45,18 +67,78 @@ class Discussion
     notes.any?(&:legacy_diff_note?)
   end
 
+  def resolvable?
+    return @resolvable if @resolvable.present?
+
+    @resolvable = diff_discussion? && notes.any?(&:resolvable?)
+  end
+
+  def resolved?
+    return @resolved if @resolved.present?
+
+    @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+  end
+
+  def first_note
+    @first_note ||= @notes.first
+  end
+
+  def last_note
+    @last_note ||= @notes.last
+  end
+
+  def resolved_notes
+    notes.select(&:resolved?)
+  end
+
+  def to_be_resolved?
+    resolvable? && !resolved?
+  end
+
+  def can_resolve?(current_user)
+    return false unless current_user
+    return false unless resolvable?
+
+    current_user == self.noteable.author ||
+      current_user.can?(:resolve_note, self.project)
+  end
+
+  def resolve!(current_user)
+    return unless resolvable?
+
+    update { |notes| notes.resolve!(current_user) }
+  end
+
+  def unresolve!
+    return unless resolvable?
+
+    update { |notes| notes.unresolve! }
+  end
+
   def for_target?(target)
     self.noteable == target && !diff_discussion?
   end
 
   def active?
-    return @active if defined?(@active)
+    return @active if @active.present?
 
     @active = first_note.active?
   end
 
+  def collapsed?
+    return false unless diff_discussion?
+
+    if resolvable?
+      # New diff discussions only disappear once they are marked resolved
+      resolved?
+    else
+      # Old diff discussions disappear once they become outdated
+      !active?
+    end
+  end
+
   def expanded?
-    !diff_discussion? || active?
+    !collapsed?
   end
 
   def reply_attributes
@@ -94,4 +176,17 @@ class Discussion
 
     prev_lines
   end
+
+  private
+
+  def update
+    notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
+    yield(notes_relation)
+
+    # Set the notes array to the updated notes
+    @notes = notes_relation.to_a
+
+    # Reset the memoized values
+    @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
+  end
 end
diff --git a/app/models/email.rb b/app/models/email.rb
index 32a412ab878aedefb63dc69d6aeddf80d789225a..826d4f16edb4a5d12b4fb198179196cde40697db 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -7,10 +7,8 @@ class Email < ActiveRecord::Base
   validates :email, presence: true, uniqueness: true, email: true
   validate :unique_email, if: ->(email) { email.email_changed? }
 
-  before_validation :cleanup_email
-
-  def cleanup_email
-    self.email = self.email.downcase.strip
+  def email=(value)
+    write_attribute(:email, value.downcase.strip)
   end
 
   def unique_email
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 75e6f869786e5bbc513ce1a677b0889fe97a6bcc..73f415c0ef07953c891cfe05abe303bca0aa7cdf 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
   has_many :deployments
 
   before_validation :nullify_external_url
+  before_save :set_environment_type
 
   validates :name,
             presence: true,
@@ -18,6 +19,24 @@ class Environment < ActiveRecord::Base
             allow_nil: true,
             addressable_url: true
 
+  delegate :stop_action, to: :last_deployment, allow_nil: true
+
+  scope :available, -> { with_state(:available) }
+  scope :stopped, -> { with_state(:stopped) }
+
+  state_machine :state, initial: :available do
+    event :start do
+      transition stopped: :available
+    end
+
+    event :stop do
+      transition available: :stopped
+    end
+
+    state :available
+    state :stopped
+  end
+
   def last_deployment
     deployments.last
   end
@@ -26,9 +45,53 @@ class Environment < ActiveRecord::Base
     self.external_url = nil if self.external_url.blank?
   end
 
+  def set_environment_type
+    names = name.split('/')
+
+    self.environment_type =
+      if names.many?
+        names.first
+      else
+        nil
+      end
+  end
+
   def includes_commit?(commit)
     return false unless last_deployment
 
     last_deployment.includes_commit?(commit)
   end
+
+  def update_merge_request_metrics?
+    self.name == "production"
+  end
+
+  def first_deployment_for(commit)
+    ref = project.repository.ref_name_for_sha(ref_path, commit.sha)
+
+    return nil unless ref
+
+    deployment_iid = ref.split('/').last
+    deployments.find_by(iid: deployment_iid)
+  end
+
+  def ref_path
+    "refs/environments/#{Shellwords.shellescape(name)}"
+  end
+
+  def formatted_external_url
+    return nil unless external_url
+
+    external_url.gsub(/\A.*?:\/\//, '')
+  end
+
+  def stoppable?
+    available? && stop_action.present?
+  end
+
+  def stop!(current_user)
+    return unless stoppable?
+
+    stop_action.play(current_user)
+  end
 end
diff --git a/app/models/event.rb b/app/models/event.rb
index fd736d123593b66b386c552b006a8146f2756d9d..c76d88b1c7b33f9d07f038887c078cbfcb46e6ee 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,6 +1,6 @@
 class Event < ActiveRecord::Base
   include Sortable
-  default_scope { where.not(author_id: nil) }
+  default_scope { reorder(nil).where.not(author_id: nil) }
 
   CREATED   = 1
   UPDATED   = 2
@@ -12,6 +12,9 @@ class Event < ActiveRecord::Base
   JOINED    = 8 # User joined project
   LEFT      = 9 # User left project
   DESTROYED = 10
+  EXPIRED   = 11 # User left project due to expiry
+
+  RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
 
   delegate :name, :email, to: :author, prefix: true, allow_nil: true
   delegate :title, to: :issue, prefix: true, allow_nil: true
@@ -46,6 +49,7 @@ class Event < ActiveRecord::Base
         update_all(updated_at: Time.now)
     end
 
+    # Update Gitlab::ContributionsCalendar#activity_dates if this changes
     def contributions
       where("action = ? OR (target_type in (?) AND action in (?))",
             Event::PUSHED, ["MergeRequest", "Issue"],
@@ -59,15 +63,17 @@ class Event < ActiveRecord::Base
 
   def visible_to_user?(user = nil)
     if push?
-      true
+      Ability.allowed?(user, :download_code, project)
     elsif membership_changed?
       true
     elsif created_project?
       true
     elsif issue? || issue_note?
-      Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
+      Ability.allowed?(user, :read_issue, note? ? note_target : target)
+    elsif merge_request? || merge_request_note?
+      Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
     else
-      ((merge_request? || note?) && target.present?) || milestone?
+      milestone?
     end
   end
 
@@ -111,6 +117,10 @@ class Event < ActiveRecord::Base
     action == LEFT
   end
 
+  def expired?
+    action == EXPIRED
+  end
+
   def destroyed?
     action == DESTROYED
   end
@@ -120,7 +130,7 @@ class Event < ActiveRecord::Base
   end
 
   def membership_changed?
-    joined? || left?
+    joined? || left? || expired?
   end
 
   def created_project?
@@ -180,6 +190,8 @@ class Event < ActiveRecord::Base
       'joined'
     elsif left?
       'left'
+    elsif expired?
+      'removed due to membership expiration from'
     elsif destroyed?
       'destroyed'
     elsif commented?
@@ -278,6 +290,10 @@ class Event < ActiveRecord::Base
     note? && target && target.for_issue?
   end
 
+  def merge_request_note?
+    note? && target && target.for_merge_request?
+  end
+
   def project_snippet_note?
     target.for_snippet?
   end
@@ -324,8 +340,22 @@ class Event < ActiveRecord::Base
   end
 
   def reset_project_activity
-    if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain
-      project.update_column(:last_activity_at, self.created_at)
-    end
+    return unless project
+
+    # Don't bother updating if we know the project was updated recently.
+    return if recent_update?
+
+    # At this point it's possible for multiple threads/processes to try to
+    # update the project. Only one query should actually perform the update,
+    # hence we add the extra WHERE clause for last_activity_at.
+    Project.unscoped.where(id: project_id).
+      where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
+      update_all(last_activity_at: created_at)
+  end
+
+  private
+
+  def recent_update?
+    project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
   end
 end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index b7894c99846040345641c5bd16ccb52441b943c5..91b508eb3251d9893c5ddae90bae4f318446dd7d 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -29,6 +29,10 @@ class ExternalIssue
     @project
   end
 
+  def project_id
+    @project.id
+  end
+
   # Pattern used to extract `JIRA-123` issue references from text
   def self.reference_pattern
     @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index ddd4bad5c216fc8d729302ab820cbd1c21ab3f3c..698a7bbd32772f39e883e3c98b2ee03948b871d3 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,6 +4,10 @@ class GlobalLabel
 
   delegate :color, :description, to: :@first_label
 
+  def for_display
+    @first_label
+  end
+
   def self.build_collection(labels)
     labels = labels.group_by(&:title)
 
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index da7c265a3718ae7a0c99aecb16a3cae0f457d2df..cde4a568577c65427d2a34f472b14707556b90b0 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -4,11 +4,16 @@ class GlobalMilestone
   attr_accessor :title, :milestones
   alias_attribute :name, :title
 
+  def for_display
+    @first_milestone
+  end
+
   def self.build_collection(milestones)
     milestones = milestones.group_by(&:title)
 
     milestones.map do |title, milestones|
-      new(title, milestones)
+      milestones_relation = Milestone.where(id: milestones.map(&:id))
+      new(title, milestones_relation)
     end
   end
 
@@ -16,6 +21,7 @@ class GlobalMilestone
     @title = title
     @name = title
     @milestones = milestones
+    @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
   end
 
   def safe_title
@@ -31,7 +37,7 @@ class GlobalMilestone
   end
 
   def projects
-    @projects ||= Project.for_milestones(milestones.map(&:id))
+    @projects ||= Project.for_milestones(milestones.select(:id))
   end
 
   def state
@@ -53,19 +59,19 @@ class GlobalMilestone
   end
 
   def issues
-    @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
+    @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
   end
 
   def merge_requests
-    @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
+    @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
   end
 
   def participants
-    @participants ||= milestones.map(&:participants).flatten.compact.uniq
+    @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
   end
 
   def labels
-    @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+    @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
                            .sort_by!(&:title)
   end
 
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b997014c644d467827dcd0986a464f5e125..d9e90cd256a9cc31e934a318b7bf695b32e11802 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -6,7 +6,7 @@ class Group < Namespace
   include AccessRequestable
   include Referable
 
-  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember'
+  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
   alias_method :members, :group_members
   has_many :users, through: :group_members
   has_many :owners,
@@ -19,6 +19,7 @@ class Group < Namespace
   has_many :project_group_links, dependent: :destroy
   has_many :shared_projects, through: :project_group_links, source: :project
   has_many :notification_settings, dependent: :destroy, as: :source
+  has_many :labels, class_name: 'GroupLabel'
 
   validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
   validate :visibility_level_allowed_by_projects
@@ -67,7 +68,7 @@ class Group < Namespace
   end
 
   def web_url
-    Gitlab::Routing.url_helpers.group_url(self)
+    Gitlab::Routing.url_helpers.group_canonical_url(self)
   end
 
   def human_name
@@ -95,34 +96,51 @@ class Group < Namespace
     end
   end
 
-  def add_users(user_ids, access_level, current_user = nil)
-    user_ids.each do |user_id|
-      Member.add_user(self.group_members, user_id, access_level, current_user)
-    end
+  def lfs_enabled?
+    return false unless Gitlab.config.lfs.enabled
+    return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+    self[:lfs_enabled]
+  end
+
+  def add_users(users, access_level, current_user: nil, expires_at: nil)
+    GroupMember.add_users_to_group(
+      self,
+      users,
+      access_level,
+      current_user: current_user,
+      expires_at: expires_at
+    )
   end
 
-  def add_user(user, access_level, current_user = nil)
-    add_users([user], access_level, current_user)
+  def add_user(user, access_level, current_user: nil, expires_at: nil)
+    GroupMember.add_user(
+      self,
+      user,
+      access_level,
+      current_user: current_user,
+      expires_at: expires_at
+    )
   end
 
   def add_guest(user, current_user = nil)
-    add_user(user, Gitlab::Access::GUEST, current_user)
+    add_user(user, :guest, current_user: current_user)
   end
 
   def add_reporter(user, current_user = nil)
-    add_user(user, Gitlab::Access::REPORTER, current_user)
+    add_user(user, :reporter, current_user: current_user)
   end
 
   def add_developer(user, current_user = nil)
-    add_user(user, Gitlab::Access::DEVELOPER, current_user)
+    add_user(user, :developer, current_user: current_user)
   end
 
   def add_master(user, current_user = nil)
-    add_user(user, Gitlab::Access::MASTER, current_user)
+    add_user(user, :master, current_user: current_user)
   end
 
   def add_owner(user, current_user = nil)
-    add_user(user, Gitlab::Access::OWNER, current_user)
+    add_user(user, :owner, current_user: current_user)
   end
 
   def has_owner?(user)
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
new file mode 100644
index 0000000000000000000000000000000000000000..68841ace2e6b62698c6f5d73e66eab45c360cb27
--- /dev/null
+++ b/app/models/group_label.rb
@@ -0,0 +1,15 @@
+class GroupLabel < Label
+  belongs_to :group
+
+  validates :group, presence: true
+
+  alias_attribute :subject, :group
+
+  def subject_foreign_key
+    'group_id'
+  end
+
+  def to_reference(source_project = nil, target_project = nil, format: :id)
+    super(source_project, target_project, format: format)
+  end
+end
diff --git a/app/models/guest.rb b/app/models/guest.rb
new file mode 100644
index 0000000000000000000000000000000000000000..01285ca12644d6d044f46f19853fc4901e775c5b
--- /dev/null
+++ b/app/models/guest.rb
@@ -0,0 +1,7 @@
+class Guest
+  class << self
+    def can?(action, subject)
+      Ability.allowed?(nil, action, subject)
+    end
+  end
+end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 836a75b0608d11ca3ae693f01fb5bc15e17dede4..c631e7a7df580ed5030bdb46747b3dc233f6f709 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@ class ProjectHook < WebHook
   belongs_to :project
 
   scope :issue_hooks, -> { where(issues_events: true) }
+  scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) }
   scope :note_hooks, -> { where(note_events: true) }
   scope :merge_request_hooks, -> { where(merge_requests_events: true) }
   scope :build_hooks, -> { where(build_events: true) }
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f365dee31418c85188ce8c638d258133c62470ac..595602e80fe6593cb154cde5ad766e6cc013f5b1 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base
 
   default_value_for :push_events, true
   default_value_for :issues_events, false
+  default_value_for :confidential_issues_events, false
   default_value_for :note_events, false
   default_value_for :merge_requests_events, false
   default_value_for :tag_push_events, false
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 788611305fec4b5b9229e556b04aa949336a8229..adbca510ef77148a24a9bf29eebbed39d239e5c2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,7 +5,6 @@ class Issue < ActiveRecord::Base
   include Issuable
   include Referable
   include Sortable
-  include Taskable
   include Spammable
   include FasterCacheKeys
 
@@ -23,6 +22,8 @@ class Issue < ActiveRecord::Base
 
   has_many :events, as: :target, dependent: :destroy
 
+  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
   validates :project, presence: true
 
   scope :cared, ->(user) { where(assignee_id: user) }
@@ -36,6 +37,8 @@ class Issue < ActiveRecord::Base
   scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
   scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
 
+  scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+
   attr_spammable :title, spam_title: true
   attr_spammable :description, spam_description: true
 
@@ -134,6 +137,10 @@ class Issue < ActiveRecord::Base
     reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
   end
 
+  def self.project_foreign_key
+    'project_id'
+  end
+
   def self.sort(method, excluded_labels: [])
     case method.to_s
     when 'due_date_asc' then order_due_date_asc
@@ -203,7 +210,13 @@ class Issue < ActiveRecord::Base
       note.all_references(current_user, extractor: ext)
     end
 
-    ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
+    merge_requests = ext.merge_requests.select(&:open?)
+    if merge_requests.any?
+      ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id)
+      merge_requests.select { |mr| mr.id.in?(ids) }
+    else
+      []
+    end
   end
 
   def moved?
@@ -237,10 +250,41 @@ class Issue < ActiveRecord::Base
   # Returns `true` if the current issue can be viewed by either a logged in User
   # or an anonymous user.
   def visible_to_user?(user = nil)
+    return false unless project.feature_available?(:issues, user)
+
     user ? readable_by?(user) : publicly_visible?
   end
 
+  def overdue?
+    due_date.try(:past?) || false
+  end
+
+  # Only issues on public projects should be checked for spam
+  def check_for_spam?
+    project.public?
+  end
+
+  def as_json(options = {})
+    super(options).tap do |json|
+      json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+
+      if options.has_key?(:labels)
+        json[:labels] = labels.as_json(
+          project: project,
+          only: [:id, :title, :description, :color, :priority],
+          methods: [:text_color]
+        )
+      end
+    end
+  end
+
+  private
+
   # Returns `true` if the given User can read the current Issue.
+  #
+  # This method duplicates the same check of issue_policy.rb
+  # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
+  # Make sure to sync this method with issue_policy.rb
   def readable_by?(user)
     if user.admin?
       true
@@ -261,13 +305,4 @@ class Issue < ActiveRecord::Base
   def publicly_visible?
     project.public? && !confidential?
   end
-
-  def overdue?
-    due_date.try(:past?) || false
-  end
-
-  # Only issues on public projects should be checked for spam
-  def check_for_spam?
-    project.public?
-  end
 end
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..012d545c44093baaf457732c236428ac9f4055f0
--- /dev/null
+++ b/app/models/issue/metrics.rb
@@ -0,0 +1,21 @@
+class Issue::Metrics < ActiveRecord::Base
+  belongs_to :issue
+
+  def record!
+    if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
+      self.first_associated_with_milestone_at = Time.now
+    end
+
+    if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
+      self.first_added_to_board_at = Time.now
+    end
+
+    self.save
+  end
+
+  private
+
+  def issue_assigned_to_list_label?
+    issue.labels.any? { |label| label.lists.present? }
+  end
+end
diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0b7d9914c80167f9c5aa94cbcaa3dd9e9b019d7
--- /dev/null
+++ b/app/models/issue_collection.rb
@@ -0,0 +1,42 @@
+# IssueCollection can be used to reduce a list of issues down to a subset.
+#
+# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
+# to take a list of issues and return a new list of issues based on some
+# criteria. For example, given a list of issues you may want to return a list of
+# issues that can be read or updated by a given user.
+class IssueCollection
+  attr_reader :collection
+
+  def initialize(collection)
+    @collection = collection
+  end
+
+  # Returns all the issues that can be updated by the user.
+  def updatable_by_user(user)
+    return collection if user.admin?
+
+    # Given all the issue projects we get a list of projects that the current
+    # user has at least reporter access to.
+    projects_with_reporter_access = user.
+      projects_with_reporter_access_limited_to(project_ids).
+      pluck(:id)
+
+    collection.select do |issue|
+      if projects_with_reporter_access.include?(issue.project_id)
+        true
+      elsif issue.is_a?(Issue)
+        issue.assignee_or_author?(user)
+      else
+        false
+      end
+    end
+  end
+
+  alias_method :visible_to, :updatable_by_user
+
+  private
+
+  def project_ids
+    @project_ids ||= collection.map(&:project_id).uniq
+  end
+end
diff --git a/app/models/label.rb b/app/models/label.rb
index 35e678001dc6c29765fa987a23d5ae45ce202139..d9287f2dc290e328003c24a5148fa5dfe591c145 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -1,4 +1,5 @@
 class Label < ActiveRecord::Base
+  include CacheMarkdownField
   include Referable
   include Subscribable
 
@@ -8,36 +9,55 @@ class Label < ActiveRecord::Base
   None = LabelStruct.new('No Label', 'No Label')
   Any = LabelStruct.new('Any Label', '')
 
+  cache_markdown_field :description, pipeline: :single_line
+
   DEFAULT_COLOR = '#428BCA'
 
   default_value_for :color, DEFAULT_COLOR
 
-  belongs_to :project
+  has_many :lists, dependent: :destroy
+  has_many :priorities, class_name: 'LabelPriority'
   has_many :label_links, dependent: :destroy
   has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
   has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
 
   validates :color, color: true, allow_blank: false
-  validates :project, presence: true, unless: Proc.new { |service| service.template? }
 
   # Don't allow ',' for label titles
-  validates :title,
-            presence: true,
-            format: { with: /\A[^,]+\z/ },
-            uniqueness: { scope: :project_id }
-
-  before_save :nullify_priority
+  validates :title, presence: true, format: { with: /\A[^,]+\z/ }
+  validates :title, uniqueness: { scope: [:group_id, :project_id] }
 
   default_scope { order(title: :asc) }
 
-  scope :templates, ->  { where(template: true) }
+  scope :templates, -> { where(template: true) }
+  scope :with_title, ->(title) { where(title: title) }
 
-  def self.prioritized
-    where.not(priority: nil).reorder(:priority, :title)
+  def self.prioritized(project)
+    joins(:priorities)
+      .where(label_priorities: { project_id: project })
+      .reorder('label_priorities.priority ASC, labels.title ASC')
   end
 
-  def self.unprioritized
-    where(priority: nil)
+  def self.unprioritized(project)
+    labels = Label.arel_table
+    priorities = LabelPriority.arel_table
+
+    label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+                              on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
+                              join_sources
+
+    joins(label_priorities).where(priorities[:priority].eq(nil))
+  end
+
+  def self.left_join_priorities
+    labels = Label.arel_table
+    priorities = LabelPriority.arel_table
+
+    label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+                              on(labels[:id].eq(priorities[:label_id])).
+                              join_sources
+
+    joins(label_priorities)
   end
 
   alias_attribute :name, :title
@@ -72,6 +92,51 @@ class Label < ActiveRecord::Base
     nil
   end
 
+  def open_issues_count(user = nil)
+    issues_count(user, state: 'opened')
+  end
+
+  def closed_issues_count(user = nil)
+    issues_count(user, state: 'closed')
+  end
+
+  def open_merge_requests_count(user = nil)
+    params = {
+      subject_foreign_key => subject.id,
+      label_name: title,
+      scope: 'all',
+      state: 'opened'
+    }
+
+    MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count
+  end
+
+  def prioritize!(project, value)
+    label_priority = priorities.find_or_initialize_by(project_id: project.id)
+    label_priority.priority = value
+    label_priority.save!
+  end
+
+  def unprioritize!(project)
+    priorities.where(project: project).delete_all
+  end
+
+  def priority(project)
+    priorities.find_by(project: project).try(:priority)
+  end
+
+  def template?
+    template
+  end
+
+  def text_color
+    LabelsHelper.text_color_for_bg(self.color)
+  end
+
+  def title=(value)
+    write_attribute(:title, sanitize_title(value)) if value.present?
+  end
+
   ##
   # Returns the String necessary to reference this Label in Markdown
   #
@@ -79,49 +144,40 @@ class Label < ActiveRecord::Base
   #
   # Examples:
   #
-  #   Label.first.to_reference                # => "~1"
-  #   Label.first.to_reference(format: :name) # => "~\"bug\""
-  #   Label.first.to_reference(project)       # => "gitlab-org/gitlab-ce~1"
+  #   Label.first.to_reference                     # => "~1"
+  #   Label.first.to_reference(format: :name)      # => "~\"bug\""
+  #   Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
   #
   # Returns a String
   #
-  def to_reference(from_project = nil, format: :id)
+  def to_reference(source_project = nil, target_project = nil, format: :id)
     format_reference = label_format_reference(format)
     reference = "#{self.class.reference_prefix}#{format_reference}"
 
-    if cross_project_reference?(from_project)
-      project.to_reference + reference
+    if cross_project_reference?(source_project, target_project)
+      source_project.to_reference + reference
     else
       reference
     end
   end
 
-  def open_issues_count(user = nil)
-    issues.visible_to_user(user).opened.count
-  end
-
-  def closed_issues_count(user = nil)
-    issues.visible_to_user(user).closed.count
-  end
-
-  def open_merge_requests_count
-    merge_requests.opened.count
+  def as_json(options = {})
+    super(options).tap do |json|
+      json[:priority] = priority(options[:project]) if options.has_key?(:project)
+    end
   end
 
-  def template?
-    template
-  end
+  private
 
-  def text_color
-    LabelsHelper::text_color_for_bg(self.color)
+  def cross_project_reference?(source_project, target_project)
+    source_project && target_project && source_project != target_project
   end
 
-  def title=(value)
-    write_attribute(:title, sanitize_title(value)) if value.present?
+  def issues_count(user, params = {})
+    params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
+    IssuesFinder.new(user, params.with_indifferent_access).execute.count
   end
 
-  private
-
   def label_format_reference(format = :id)
     raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
 
@@ -132,10 +188,6 @@ class Label < ActiveRecord::Base
     end
   end
 
-  def nullify_priority
-    self.priority = nil if priority.blank?
-  end
-
   def sanitize_title(value)
     CGI.unescapeHTML(Sanitize.clean(value.to_s))
   end
diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5b85e0b6533644ea7d886b315f0e42a9278de975
--- /dev/null
+++ b/app/models/label_priority.rb
@@ -0,0 +1,8 @@
+class LabelPriority < ActiveRecord::Base
+  belongs_to :project
+  belongs_to :label
+
+  validates :project, :label, :priority, presence: true
+  validates :label_id, uniqueness: { scope: :project_id }
+  validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 6ed6600151385023f3c816b767bd221dbd2f1afb..40277a9b13963f1d7a44bc83a0bf9d76f2758165 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -8,8 +8,8 @@ class LegacyDiffNote < Note
   before_create :set_diff
 
   class << self
-    def build_discussion_id(noteable_type, noteable_id, line_code, active = true)
-      [super(noteable_type, noteable_id), line_code, active].join("-")
+    def build_discussion_id(noteable_type, noteable_id, line_code)
+      [super(noteable_type, noteable_id), line_code].join("-")
     end
   end
 
@@ -21,10 +21,6 @@ class LegacyDiffNote < Note
     { line_code: line_code }
   end
 
-  def discussion_id
-    @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
-  end
-
   def project_repository
     if RequestStore.active?
       RequestStore.fetch("project:#{project_id}:repository") { self.project.repository }
@@ -53,6 +49,10 @@ class LegacyDiffNote < Note
     !line.meta? && diff_file.line_code(line) == self.line_code
   end
 
+  def original_line_code
+    self.line_code
+  end
+
   # Check if this note is part of an "active" discussion
   #
   # This will always return true for anything except MergeRequest noteables,
@@ -119,4 +119,8 @@ class LegacyDiffNote < Note
     diffs = noteable.raw_diffs(Commit.max_diff_options)
     diffs.find { |d| d.new_path == self.diff.new_path }
   end
+
+  def build_discussion_id
+    self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
+  end
 end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 18657c3e1c893e68dbc5ad215b56a0f7fe3c48c6..7712d5783e049d0d2121b6559603669a2b9c9da9 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -17,4 +17,10 @@ class LfsObject < ActiveRecord::Base
   def project_allowed_access?(project)
     projects.exists?(storage_project(project).id)
   end
+
+  def self.destroy_unreferenced
+    joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
+        .where(lfs_objects_projects: { id: nil })
+        .destroy_all
+  end
 end
diff --git a/app/models/list.rb b/app/models/list.rb
new file mode 100644
index 0000000000000000000000000000000000000000..065d75bd1dc6bbd3aace6d3cc34ccbc60ac6e5b1
--- /dev/null
+++ b/app/models/list.rb
@@ -0,0 +1,45 @@
+class List < ActiveRecord::Base
+  belongs_to :board
+  belongs_to :label
+
+  enum list_type: { backlog: 0, label: 1, done: 2 }
+
+  validates :board, :list_type, presence: true
+  validates :label, :position, presence: true, if: :label?
+  validates :label_id, uniqueness: { scope: :board_id }, if: :label?
+  validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+
+  before_destroy :can_be_destroyed
+
+  scope :destroyable, -> { where(list_type: list_types[:label]) }
+  scope :movable, -> { where(list_type: list_types[:label]) }
+
+  def destroyable?
+    label?
+  end
+
+  def movable?
+    label?
+  end
+
+  def title
+    label? ? label.name : list_type.humanize
+  end
+
+  def as_json(options = {})
+    super(options).tap do |json|
+      if options.has_key?(:label)
+        json[:label] = label.as_json(
+          project: board.project,
+          only: [:id, :title, :description, :color]
+        )
+      end
+    end
+  end
+
+  private
+
+  def can_be_destroyed
+    destroyable?
+  end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee936f9a81098648802df78c9a8e585a..b89ba8ecbb819efff8e459a5eb88a816f42c8a3b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
 class Member < ActiveRecord::Base
   include Sortable
   include Importable
+  include Expirable
   include Gitlab::Access
 
   attr_accessor :raw_invite_token
@@ -27,17 +28,34 @@ class Member < ActiveRecord::Base
       allow_nil: true
     }
 
+  # This scope encapsulates (most of) the conditions a row in the member table
+  # must satisfy if it is a valid permission. Of particular note:
+  #
+  #   * Access requests must be excluded
+  #   * Blocked users must be excluded
+  #   * Invitations take effect immediately
+  #   * expires_at is not implemented. A background worker purges expired rows
+  scope :active, -> do
+    is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
+    user_is_active = User.arel_table[:state].eq(:active)
+
+    includes(:user).references(:users)
+      .where(is_external_invite.or(user_is_active))
+      .where(requested_at: nil)
+  end
+
   scope :invite, -> { where.not(invite_token: nil) }
   scope :non_invite, -> { where(invite_token: nil) }
   scope :request, -> { where.not(requested_at: nil) }
-  scope :has_access, -> { where('access_level > 0') }
 
-  scope :guests, -> { where(access_level: GUEST) }
-  scope :reporters, -> { where(access_level: REPORTER) }
-  scope :developers, -> { where(access_level: DEVELOPER) }
-  scope :masters,  -> { where(access_level: MASTER) }
-  scope :owners,  -> { where(access_level: OWNER) }
-  scope :owners_and_masters,  -> { where(access_level: [OWNER, MASTER]) }
+  scope :has_access, -> { active.where('access_level > 0') }
+
+  scope :guests, -> { active.where(access_level: GUEST) }
+  scope :reporters, -> { active.where(access_level: REPORTER) }
+  scope :developers, -> { active.where(access_level: DEVELOPER) }
+  scope :masters,  -> { active.where(access_level: MASTER) }
+  scope :owners,  -> { active.where(access_level: OWNER) }
+  scope :owners_and_masters,  -> { active.where(access_level: [OWNER, MASTER]) }
 
   before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
 
@@ -62,48 +80,75 @@ class Member < ActiveRecord::Base
       find_by(invite_token: invite_token)
     end
 
-    # This method is used to find users that have been entered into the "Add members" field.
-    # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
-    def user_for_id(user_id)
-      return user_id if user_id.is_a?(User)
-
-      user = User.find_by(id: user_id)
-      user ||= User.find_by(email: user_id)
-      user ||= user_id
-      user
-    end
-
-    def add_user(members, user_id, access_level, current_user = nil)
-      user = user_for_id(user_id)
+    def add_user(source, user, access_level, current_user: nil, expires_at: nil)
+      user = retrieve_user(user)
+      access_level = retrieve_access_level(access_level)
 
       # `user` can be either a User object or an email to be invited
-      if user.is_a?(User)
-        member = members.find_or_initialize_by(user_id: user.id)
+      member =
+        if user.is_a?(User)
+          source.members.find_by(user_id: user.id) ||
+          source.requesters.find_by(user_id: user.id) ||
+          source.members.build(user_id: user.id)
+        else
+          source.members.build(invite_email: user)
+        end
+
+      return member unless can_update_member?(current_user, member)
+
+      member.attributes = {
+        created_by: member.created_by || current_user,
+        access_level: access_level,
+        expires_at: expires_at
+      }
+
+      if member.request?
+        ::Members::ApproveAccessRequestService.new(
+          source,
+          current_user,
+          id: member.id,
+          access_level: access_level
+        ).execute
       else
-        member = members.build
-        member.invite_email = user
+        member.save
       end
 
-      if can_update_member?(current_user, member) || project_creator?(member, access_level)
-        member.created_by ||= current_user
-        member.access_level = access_level
+      member
+    end
 
-        member.save
-      end
+    def access_levels
+      Gitlab::Access.sym_options
     end
 
     private
 
+    # This method is used to find users that have been entered into the "Add members" field.
+    # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
+    def retrieve_user(user)
+      return user if user.is_a?(User)
+
+      User.find_by(id: user) || User.find_by(email: user) || user
+    end
+
+    def retrieve_access_level(access_level)
+      access_levels.fetch(access_level) { access_level.to_i }
+    end
+
     def can_update_member?(current_user, member)
       # There is no current user for bulk actions, in which case anything is allowed
-      !current_user ||
-        current_user.can?(:update_group_member, member) ||
-        current_user.can?(:update_project_member, member)
+      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
     end
 
-    def project_creator?(member, access_level)
-      member.new_record? && member.owner? &&
-        access_level.to_i == ProjectMember::MASTER
+    def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
+      users.each do |user|
+        add_user(
+          source,
+          user,
+          access_level,
+          current_user: current_user,
+          expires_at: expires_at
+        )
+      end
     end
   end
 
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2f13d339c8970af9a6936f8c59c0563cbdcfb830..204f34f026944a967978159e98e7caf0a60e8cd5 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,7 +1,7 @@
 class GroupMember < Member
   SOURCE_TYPE = 'Namespace'
 
-  belongs_to :group, class_name: 'Group', foreign_key: 'source_id'
+  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
@@ -12,6 +12,22 @@ class GroupMember < Member
     Gitlab::Access.options_with_owner
   end
 
+  def self.access_levels
+    Gitlab::Access.sym_options_with_owner
+  end
+
+  def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
+    self.transaction do
+      add_users_to_source(
+        group,
+        users,
+        access_level,
+        current_user: current_user,
+        expires_at: expires_at
+      )
+    end
+  end
+
   def group
     source
   end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 18e97c969d76d9286fa18f6e52be80ed08693ebb..008fff0857c35af5748033ba9b4b0347c1321516 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -3,7 +3,7 @@ class ProjectMember < Member
 
   include Gitlab::ShellAdapter
 
-  belongs_to :project, class_name: 'Project', foreign_key: 'source_id'
+  belongs_to :project, foreign_key: 'source_id'
 
   # Make sure project member points only to project as it source
   default_value_for :source_type, SOURCE_TYPE
@@ -34,30 +34,20 @@ class ProjectMember < Member
     #     :master
     #   )
     #
-    def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
-      access_level = if roles_hash.has_key?(access)
-                       roles_hash[access]
-                     elsif roles_hash.values.include?(access.to_i)
-                       access
-                     else
-                       raise "Non valid access"
-                     end
-
-      users = user_ids.map { |user_id| Member.user_for_id(user_id) }
-
-      ProjectMember.transaction do
+    def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
+      self.transaction do
         project_ids.each do |project_id|
           project = Project.find(project_id)
 
-          users.each do |user|
-            Member.add_user(project.project_members, user, access_level, current_user)
-          end
+          add_users_to_source(
+            project,
+            users,
+            access_level,
+            current_user: current_user,
+            expires_at: expires_at
+          )
         end
       end
-
-      true
-    rescue
-      false
     end
 
     def truncate_teams(project_ids)
@@ -78,13 +68,15 @@ class ProjectMember < Member
       truncate_teams [project.id]
     end
 
-    def roles_hash
-      Gitlab::Access.sym_options
-    end
-
     def access_level_roles
       Gitlab::Access.options
     end
+
+    private
+
+    def can_update_member?(current_user, member)
+      super || (member.owner? && member.new_record?)
+    end
   end
 
   def access_field
@@ -129,7 +121,11 @@ class ProjectMember < Member
   end
 
   def post_destroy_hook
-    event_service.leave_project(self.project, self.user)
+    if expired?
+      event_service.expired_leave_project(self.project, self.user)
+    else
+      event_service.leave_project(self.project, self.user)
+    end
 
     super
   end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index fe799382fd0083bc7f44516f5cb1260ff465e992..d76feb9680e0141e79fae49ecc5a826255aeb4fc 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,21 +3,24 @@ class MergeRequest < ActiveRecord::Base
   include Issuable
   include Referable
   include Sortable
-  include Taskable
   include Importable
 
-  belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
-  belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
+  belongs_to :target_project, class_name: "Project"
+  belongs_to :source_project, class_name: "Project"
   belongs_to :merge_user, class_name: "User"
 
-  has_one :merge_request_diff, dependent: :destroy
+  has_many :merge_request_diffs, dependent: :destroy
+  has_one :merge_request_diff,
+    -> { order('merge_request_diffs.id DESC') }
 
   has_many :events, as: :target, dependent: :destroy
 
+  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
   serialize :merge_params, Hash
 
-  after_create :create_merge_request_diff, unless: :importing?
-  after_update :update_merge_request_diff
+  after_create :ensure_merge_request_diff, unless: :importing?
+  after_update :reload_diff_if_branch_changed
 
   delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
 
@@ -27,7 +30,7 @@ class MergeRequest < ActiveRecord::Base
 
   # Temporary fields to store compare vars
   # when creating new merge request
-  attr_accessor :can_be_created, :compare_commits, :compare
+  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
 
   state_machine :state, initial: :opened do
     event :close do
@@ -89,13 +92,13 @@ class MergeRequest < ActiveRecord::Base
     end
   end
 
-  validates :source_project, presence: true, unless: [:allow_broken, :importing?]
+  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?
-  validate :validate_branches, unless: [:allow_broken, :importing?]
-  validate :validate_fork
+  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
+  validate :validate_fork, unless: :closed_without_fork?
 
   scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
   scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
@@ -133,6 +136,10 @@ class MergeRequest < ActiveRecord::Base
     reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
   end
 
+  def self.project_foreign_key
+    'target_project_id'
+  end
+
   # Returns all the merge requests from an ActiveRecord:Relation.
   #
   # This method uses a UNION as it usually operates on the result of
@@ -151,6 +158,20 @@ class MergeRequest < ActiveRecord::Base
     where("merge_requests.id IN (#{union.to_sql})")
   end
 
+  WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+
+  def self.work_in_progress?(title)
+    !!(title =~ WIP_REGEX)
+  end
+
+  def self.wipless_title(title)
+    title.sub(WIP_REGEX, "")
+  end
+
+  def self.wip_title(title)
+    work_in_progress?(title) ? title : "WIP: #{title}"
+  end
+
   def to_reference(from_project = nil)
     reference = "#{self.class.reference_prefix}#{iid}"
 
@@ -170,22 +191,22 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def diffs(diff_options = nil)
-    if self.compare
-      self.compare.diffs(diff_options)
+    if compare
+      compare.diffs(diff_options)
     else
-      Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options)
+      merge_request_diff.diffs(diff_options)
     end
   end
 
   def diff_size
-    merge_request_diff.size
+    diffs(diff_options).size
   end
 
   def diff_base_commit
     if persisted?
       merge_request_diff.base_commit
-    elsif diff_start_commit && diff_head_commit
-      self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
+    else
+      branch_merge_base_commit
     end
   end
 
@@ -238,12 +259,21 @@ class MergeRequest < ActiveRecord::Base
 
   def source_branch_head
     source_branch_ref = @source_branch_sha || source_branch
-    source_project.repository.commit(source_branch) if source_branch_ref
+    source_project.repository.commit(source_branch_ref) if source_branch_ref
   end
 
   def target_branch_head
     target_branch_ref = @target_branch_sha || target_branch
-    target_project.repository.commit(target_branch) if target_branch_ref
+    target_project.repository.commit(target_branch_ref) if target_branch_ref
+  end
+
+  def branch_merge_base_commit
+    start_sha = target_branch_sha
+    head_sha  = source_branch_sha
+
+    if start_sha && head_sha
+      target_project.merge_base_commit(start_sha, head_sha)
+    end
   end
 
   def target_branch_sha
@@ -267,16 +297,16 @@ class MergeRequest < ActiveRecord::Base
   # Return diff_refs instance trying to not touch the git repository
   def diff_sha_refs
     if merge_request_diff && merge_request_diff.diff_refs_by_sha?
-      return Gitlab::Diff::DiffRefs.new(
-        base_sha:  merge_request_diff.base_commit_sha,
-        start_sha: merge_request_diff.start_commit_sha,
-        head_sha:  merge_request_diff.head_commit_sha
-      )
+      merge_request_diff.diff_refs
     else
       diff_refs
     end
   end
 
+  def branch_merge_base_sha
+    branch_merge_base_commit.try(:sha)
+  end
+
   def validate_branches
     if target_project == source_project && target_branch == source_branch
       errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -294,36 +324,53 @@ class MergeRequest < ActiveRecord::Base
 
   def validate_fork
     return true unless target_project && source_project
+    return true if target_project == source_project
+    return true unless source_project_missing?
 
-    if target_project == source_project
-      true
-    else
-      # If source and target projects are different
-      # we should check if source project is actually a fork of target project
-      if source_project.forked_from?(target_project)
-        true
-      else
-        errors.add :validate_fork,
-                   'Source project is not a fork of target project'
-      end
-    end
+    errors.add :validate_fork,
+               'Source project is not a fork of the target project'
   end
 
-  def update_merge_request_diff
+  def closed_without_fork?
+    closed? && source_project_missing?
+  end
+
+  def source_project_missing?
+    return false unless for_fork?
+    return true unless source_project
+
+    !source_project.forked_from?(target_project)
+  end
+
+  def reopenable?
+    closed? && !source_project_missing? && source_branch_exists?
+  end
+
+  def ensure_merge_request_diff
+    merge_request_diff || create_merge_request_diff
+  end
+
+  def create_merge_request_diff
+    merge_request_diffs.create
+    reload_merge_request_diff
+  end
+
+  def reload_merge_request_diff
+    merge_request_diff(true)
+  end
+
+  def reload_diff_if_branch_changed
     if source_branch_changed? || target_branch_changed?
       reload_diff
     end
   end
 
   def reload_diff
-    return unless merge_request_diff && open?
+    return unless open?
 
     old_diff_refs = self.diff_refs
-
-    merge_request_diff.reload_content
-
+    create_merge_request_diff
     MergeRequests::MergeRequestDiffCacheService.new.execute(self)
-
     new_diff_refs = self.diff_refs
 
     update_diff_notes_positions(
@@ -353,14 +400,16 @@ class MergeRequest < ActiveRecord::Base
     @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
   end
 
-  WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
-
   def work_in_progress?
-    !!(title =~ WIP_REGEX)
+    self.class.work_in_progress?(title)
   end
 
   def wipless_title
-    self.title.sub(WIP_REGEX, "")
+    self.class.wipless_title(self.title)
+  end
+
+  def wip_title
+    self.class.wip_title(self.title)
   end
 
   def mergeable?(skip_ci_check: false)
@@ -376,6 +425,7 @@ class MergeRequest < ActiveRecord::Base
     return false if work_in_progress?
     return false if broken?
     return false unless skip_ci_check || mergeable_ci_state?
+    return false unless mergeable_discussions_state?
 
     true
   end
@@ -387,16 +437,16 @@ class MergeRequest < ActiveRecord::Base
   def can_remove_source_branch?(current_user)
     !source_project.protected_branch?(source_branch) &&
       !source_project.root_ref?(source_branch) &&
-      Ability.abilities.allowed?(current_user, :push_code, source_project) &&
+      Ability.allowed?(current_user, :push_code, source_project) &&
       diff_head_commit == source_branch_head
   end
 
   def should_remove_source_branch?
-    merge_params['should_remove_source_branch'].present?
+    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
   end
 
   def force_remove_source_branch?
-    merge_params['force_remove_source_branch'].present?
+    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
   end
 
   def remove_source_branch?
@@ -418,6 +468,38 @@ class MergeRequest < ActiveRecord::Base
     )
   end
 
+  def discussions
+    @discussions ||= self.mr_and_commit_notes.
+      inc_relations_for_view.
+      fresh.
+      discussions
+  end
+
+  def diff_discussions
+    @diff_discussions ||= self.notes.diff_notes.discussions
+  end
+
+  def find_diff_discussion(discussion_id)
+    notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
+    return if notes.empty?
+
+    Discussion.new(notes)
+  end
+
+  def discussions_resolvable?
+    diff_discussions.any?(&:resolvable?)
+  end
+
+  def discussions_resolved?
+    discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
+  end
+
+  def mergeable_discussions_state?
+    return true unless project.only_allow_merge_if_all_discussions_are_resolved?
+
+    discussions_resolved?
+  end
+
   def hook_attrs
     attrs = {
       source: source_project.try(:hook_attrs),
@@ -441,6 +523,23 @@ class MergeRequest < ActiveRecord::Base
     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
+  # running `ReferenceExtractor` on each of them separately.
+  # This optimization does not apply to issues from external sources.
+  def cache_merge_request_closes_issues!(current_user = self.author)
+    return if project.has_external_issue_tracker?
+
+    transaction do
+      self.merge_requests_closing_issues.delete_all
+
+      closes_issues(current_user).each do |issue|
+        self.merge_requests_closing_issues.create!(issue: issue)
+      end
+    end
+  end
+
   def closes_issue?(issue)
     closes_issues.include?(issue)
   end
@@ -448,7 +547,8 @@ class MergeRequest < ActiveRecord::Base
   # Return the set of issues that will be closed if this merge request is accepted.
   def closes_issues(current_user = self.author)
     if target_branch == project.default_branch
-      messages = commits.map(&:safe_message) << description
+      messages = [description]
+      messages.concat(commits.map(&:safe_message)) if merge_request_diff
 
       Gitlab::ClosingIssueExtractor.new(project, current_user).
         closed_by_message(messages.join("\n"))
@@ -514,13 +614,11 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def merge_commit_message
-    message = "Merge branch '#{source_branch}' into '#{target_branch}'"
-    message << "\n\n"
-    message << title.to_s
-    message << "\n\n"
-    message << description.to_s
-    message << "\n\n"
-    message << "See merge request !#{iid}"
+    message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
+    message << "#{title}\n\n"
+    message << "#{description}\n\n" if description.present?
+    message << "See merge request #{to_reference}"
+
     message
   end
 
@@ -564,7 +662,7 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def has_ci?
-    source_project.ci_service && commits.any?
+    source_project.try(:ci_service) && commits.any?
   end
 
   def branch_missing?
@@ -592,11 +690,14 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def environments
-    return unless diff_head_commit
+    return [] unless diff_head_commit
 
-    target_project.environments.select do |environment|
-      environment.includes_commit?(diff_head_commit)
-    end
+    @environments ||=
+      begin
+        envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
+        envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
+        envs.uniq
+      end
   end
 
   def state_human_name
@@ -674,8 +775,34 @@ class MergeRequest < ActiveRecord::Base
     diverged_commits_count > 0
   end
 
+  def commits_sha
+    commits.map(&:sha)
+  end
+
   def pipeline
-    @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
+    return unless diff_head_sha && source_project
+
+    @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
+  end
+
+  def all_pipelines
+    return unless source_project
+
+    @all_pipelines ||= source_project.pipelines
+      .where(sha: all_commits_sha, ref: source_branch)
+      .order(id: :desc)
+  end
+
+  # Note that this could also return SHA from now dangling commits
+  #
+  def all_commits_sha
+    if persisted?
+      merge_request_diffs.flat_map(&:commits_sha).uniq
+    elsif compare_commits
+      compare_commits.to_a.reverse.map(&:id)
+    else
+      [diff_head_sha]
+    end
   end
 
   def merge_commit
@@ -690,12 +817,12 @@ class MergeRequest < ActiveRecord::Base
     merge_commit
   end
 
-  def support_new_diff_notes?
+  def has_complete_diff_refs?
     diff_sha_refs && diff_sha_refs.complete?
   end
 
   def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
-    return unless support_new_diff_notes?
+    return unless has_complete_diff_refs?
     return if new_diff_refs == old_diff_refs
 
     active_diff_notes = self.notes.diff_notes.select do |note|
@@ -723,4 +850,30 @@ class MergeRequest < ActiveRecord::Base
   def keep_around_commit
     project.repository.keep_around(self.merge_commit_sha)
   end
+
+  def conflicts
+    @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
+  end
+
+  def conflicts_can_be_resolved_by?(user)
+    access = ::Gitlab::UserAccess.new(user, project: source_project)
+    access.can_push_to_branch?(source_branch)
+  end
+
+  def conflicts_can_be_resolved_in_ui?
+    return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
+
+    return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
+    return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
+
+    begin
+      # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
+      # ensure that we don't say there are conflicts to resolve when there are no conflict
+      # files.
+      conflicts.files.each(&:lines)
+      @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
+    rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+      @conflicts_can_be_resolved_in_ui = false
+    end
+  end
 end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99c49a020c974ef3d2e1873886b11cbb21e2831c
--- /dev/null
+++ b/app/models/merge_request/metrics.rb
@@ -0,0 +1,11 @@
+class MergeRequest::Metrics < ActiveRecord::Base
+  belongs_to :merge_request
+
+  def record!
+    if merge_request.merged? && self.merged_at.blank?
+      self.merged_at = Time.now
+    end
+
+    self.save
+  end
+end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 32cc6a3bfead9693385210c04a5b11bc3fbe5f53..dd65a9a8b86085c06fc87847cfd59bc5c56d1e68 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,9 +6,10 @@ class MergeRequestDiff < ActiveRecord::Base
   # Prevent store of diff if commits amount more then 500
   COMMITS_SAFE_SIZE = 100
 
-  belongs_to :merge_request
+  # Valid types of serialized diffs allowed by Gitlab::Git::Diff
+  VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
 
-  delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
+  belongs_to :merge_request
 
   state_machine :state, initial: :empty do
     state :collected
@@ -24,12 +25,51 @@ class MergeRequestDiff < ActiveRecord::Base
   serialize :st_commits
   serialize :st_diffs
 
-  after_create :reload_content, unless: :importing?
-  after_save :keep_around_commits, unless: :importing?
+  # All diff information is collected from repository after object is created.
+  # It allows you to override variables like head_commit_sha before getting diff.
+  after_create :save_git_content, unless: :importing?
+
+  def self.select_without_diff
+    select(column_names - ['st_diffs'])
+  end
+
+  def st_commits
+    super || []
+  end
 
-  def reload_content
+  # Collect information about commits and diff from repository
+  # and save it to the database as serialized data
+  def save_git_content
+    ensure_commits_sha
+    save_commits
     reload_commits
-    reload_diffs
+    save_diffs
+    keep_around_commits
+  end
+
+  def ensure_commits_sha
+    merge_request.fetch_ref
+    self.start_commit_sha ||= merge_request.target_branch_sha
+    self.head_commit_sha  ||= merge_request.source_branch_sha
+    self.base_commit_sha  ||= find_base_sha
+    save
+  end
+
+  # Override head_commit_sha to keep compatibility with merge request diff
+  # created before version 8.4 that does not store head_commit_sha in separate db field.
+  def head_commit_sha
+    if persisted? && super.nil?
+      last_commit.try(:sha)
+    else
+      super
+    end
+  end
+
+  # This method will rely on repository branch sha
+  # in case start_commit_sha is nil. Its necesarry for old merge request diff
+  # created before version 8.4 to work
+  def safe_start_commit_sha
+    start_commit_sha || merge_request.target_branch_sha
   end
 
   def size
@@ -38,14 +78,11 @@ class MergeRequestDiff < ActiveRecord::Base
 
   def raw_diffs(options = {})
     if options[:ignore_whitespace_change]
-      @raw_diffs_no_whitespace ||= begin
-        compare = Gitlab::Git::Compare.new(
+      @diffs_no_whitespace ||=
+        Gitlab::Git::Compare.new(
           repository.raw_repository,
-          self.start_commit_sha || self.target_branch_sha,
-          self.head_commit_sha || self.source_branch_sha,
-        )
-        compare.diffs(options)
-      end
+          safe_start_commit_sha,
+          head_commit_sha).diffs(options)
     else
       @raw_diffs ||= {}
       @raw_diffs[options] ||= load_diffs(st_diffs, options)
@@ -53,7 +90,12 @@ class MergeRequestDiff < ActiveRecord::Base
   end
 
   def commits
-    @commits ||= load_commits(st_commits || [])
+    @commits ||= load_commits(st_commits)
+  end
+
+  def reload_commits
+    @commits = nil
+    commits
   end
 
   def last_commit
@@ -65,53 +107,82 @@ class MergeRequestDiff < ActiveRecord::Base
   end
 
   def base_commit
-    return unless self.base_commit_sha
+    return unless base_commit_sha
 
-    project.commit(self.base_commit_sha)
+    project.commit(base_commit_sha)
   end
 
   def start_commit
-    return unless self.start_commit_sha
+    return unless start_commit_sha
 
-    project.commit(self.start_commit_sha)
+    project.commit(start_commit_sha)
   end
 
   def head_commit
-    return last_commit unless self.head_commit_sha
+    return unless head_commit_sha
+
+    project.commit(head_commit_sha)
+  end
+
+  def commits_sha
+    if @commits
+      commits.map(&:sha)
+    else
+      st_commits.map { |commit| commit[:id] }
+    end
+  end
+
+  def diff_refs
+    return unless start_commit_sha || base_commit_sha
 
-    project.commit(self.head_commit_sha)
+    Gitlab::Diff::DiffRefs.new(
+      base_sha:  base_commit_sha,
+      start_sha: start_commit_sha,
+      head_sha:  head_commit_sha
+    )
   end
 
   def diff_refs_by_sha?
     base_commit_sha? && head_commit_sha? && start_commit_sha?
   end
 
+  def diffs(diff_options = nil)
+    Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
+  end
+
+  def project
+    merge_request.target_project
+  end
+
   def compare
     @compare ||=
-      begin
-        # Update ref for merge request
-        merge_request.fetch_ref
+      Gitlab::Git::Compare.new(
+        repository.raw_repository,
+        safe_start_commit_sha,
+        head_commit_sha
+      )
+  end
 
-        Gitlab::Git::Compare.new(
-          repository.raw_repository,
-          self.target_branch_sha,
-          self.source_branch_sha
-        )
-      end
+  def latest?
+    self == merge_request.merge_request_diff
   end
 
-  private
+  def compare_with(sha, straight: true)
+    # When compare merge request versions we want diff A..B instead of A...B
+    # so we handle cases when user does squash and rebase of the commits between versions.
+    # For this reason we set straight to true by default.
+    CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
+  end
 
-  # Collect array of Git::Commit objects
-  # between target and source branches
-  def unmerged_commits
-    commits = compare.commits
+  private
 
-    if commits.present?
-      commits = Commit.decorate(commits, merge_request.source_project).reverse
-    end
+  # Old GitLab implementations may have generated diffs as ["--broken-diff"].
+  # Avoid an error 500 by ignoring bad elements. See:
+  # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
+  def valid_raw_diff?(raw)
+    return false unless raw.respond_to?(:each)
 
-    commits
+    raw.any? { |element| VALID_CLASSES.include?(element.class) }
   end
 
   def dump_commits(commits)
@@ -122,26 +193,21 @@ class MergeRequestDiff < ActiveRecord::Base
     array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
   end
 
-  # Reload all commits related to current merge request from repo
+  # Load all commits related to current merge request diff from repo
   # and save it as array of hashes in st_commits db field
-  def reload_commits
+  def save_commits
     new_attributes = {}
 
-    commit_objects = unmerged_commits
+    commits = compare.commits
 
-    if commit_objects.present?
-      new_attributes[:st_commits] = dump_commits(commit_objects)
+    if commits.present?
+      commits = Commit.decorate(commits, merge_request.source_project).reverse
+      new_attributes[:st_commits] = dump_commits(commits)
     end
 
     update_columns_serialized(new_attributes)
   end
 
-  # Collect array of Git::Diff objects
-  # between target and source branches
-  def unmerged_diffs
-    compare.diffs(Commit.max_diff_options)
-  end
-
   def dump_diffs(diffs)
     if diffs.respond_to?(:map)
       diffs.map(&:to_hash)
@@ -149,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base
   end
 
   def load_diffs(raw, options)
-    if raw.respond_to?(:each)
+    if valid_raw_diff?(raw)
       if paths = options[:paths]
         raw = raw.select do |diff|
           paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
@@ -162,16 +228,16 @@ class MergeRequestDiff < ActiveRecord::Base
     end
   end
 
-  # Reload diffs between branches related to current merge request from repo
+  # Load diffs between branches related to current merge request diff from repo
   # and save it as array of hashes in st_diffs db field
-  def reload_diffs
+  def save_diffs
     new_attributes = {}
     new_diffs = []
 
     if commits.size.zero?
       new_attributes[:state] = :empty
     else
-      diff_collection = unmerged_diffs
+      diff_collection = compare.diffs(Commit.max_diff_options)
 
       if diff_collection.overflow?
         # Set our state to 'overflow' to make the #empty? and #collected?
@@ -188,32 +254,17 @@ class MergeRequestDiff < ActiveRecord::Base
     end
 
     new_attributes[:st_diffs] = new_diffs
-
-    new_attributes[:start_commit_sha] = self.target_branch_sha
-    new_attributes[:head_commit_sha] = self.source_branch_sha
-    new_attributes[:base_commit_sha] = branch_base_sha
-
     update_columns_serialized(new_attributes)
-
-    keep_around_commits
-  end
-
-  def project
-    merge_request.target_project
   end
 
   def repository
     project.repository
   end
 
-  def branch_base_commit
-    return unless self.source_branch_sha && self.target_branch_sha
+  def find_base_sha
+    return unless head_commit_sha && start_commit_sha
 
-    project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
-  end
-
-  def branch_base_sha
-    branch_base_commit.try(:sha)
+    project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
   end
 
   def utf8_st_diffs
@@ -248,8 +299,10 @@ class MergeRequestDiff < ActiveRecord::Base
   end
 
   def keep_around_commits
-    repository.keep_around(target_branch_sha)
-    repository.keep_around(source_branch_sha)
-    repository.keep_around(branch_base_sha)
+    [repository, merge_request.source_project.repository].each do |repo|
+      repo.keep_around(start_commit_sha)
+      repo.keep_around(head_commit_sha)
+      repo.keep_around(base_commit_sha)
+    end
   end
 end
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab597c379471afa8becb49d0bcfaaa78137b8239
--- /dev/null
+++ b/app/models/merge_requests_closing_issues.rb
@@ -0,0 +1,7 @@
+class MergeRequestsClosingIssues < ActiveRecord::Base
+  belongs_to :merge_request
+  belongs_to :issue
+
+  validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
+  validates :issue_id, presence: true
+end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2bd7f1980306197886e0159267d6caa26dd74281..23aecbfa3a6ac9ee45e110dc73ebb2226349a9a4 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
   Any = MilestoneStruct.new('Any Milestone', '', -1)
   Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
 
+  include CacheMarkdownField
   include InternalId
   include Sortable
   include Referable
   include StripAttribute
   include Milestoneish
 
+  cache_markdown_field :title, pipeline: :single_line
+  cache_markdown_field :description
+
   belongs_to :project
   has_many :issues
   has_many :labels, -> { distinct.reorder('labels.title') },  through: :issues
@@ -158,7 +162,7 @@ class Milestone < ActiveRecord::Base
   end
 
   def title=(value)
-    write_attribute(:title, Sanitize.clean(value.to_s)) if value.present?
+    write_attribute(:title, sanitize_title(value)) if value.present?
   end
 
   # Sorts the issues for the given IDs.
@@ -204,4 +208,8 @@ class Milestone < ActiveRecord::Base
       iid
     end
   end
+
+  def sanitize_title(value)
+    CGI.unescape_html(Sanitize.clean(value.to_s))
+  end
 end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7c29d27ce9784c954960a7c0bf119b3ed79e5f0c..b67049f0f55cb6273f592c1ba4ab0244a202951c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,9 +1,12 @@
 class Namespace < ActiveRecord::Base
   acts_as_paranoid
 
+  include CacheMarkdownField
   include Sortable
   include Gitlab::ShellAdapter
 
+  cache_markdown_field :description, pipeline: :description
+
   has_many :projects, dependent: :destroy
   belongs_to :owner, class_name: "User"
 
@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
     def clean_path(path)
       path = path.dup
       # Get the email username by removing everything after an `@` sign.
-      path.gsub!(/@.*\z/,             "")
-      # Usernames can't end in .git, so remove it.
-      path.gsub!(/\.git\z/,           "")
-      # Remove dashes at the start of the username.
-      path.gsub!(/\A-+/,              "")
-      # Remove periods at the end of the username.
-      path.gsub!(/\.+\z/,             "")
+      path.gsub!(/@.*\z/,                "")
       # Remove everything that's not in the list of allowed characters.
-      path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+      path.gsub!(/[^a-zA-Z0-9_\-\.]/,    "")
+      # Remove trailing violations ('.atom', '.git', or '.')
+      path.gsub!(/(\.atom|\.git|\.)*\z/, "")
+      # Remove leading violations ('-')
+      path.gsub!(/\A\-+/,                "")
 
       # Users with the great usernames of "." or ".." would end up with a blank username.
       # Work around that by setting their username to "blank", followed by a counter.
@@ -141,6 +142,11 @@ class Namespace < ActiveRecord::Base
     projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
   end
 
+  def lfs_enabled?
+    # User namespace will always default to the global setting
+    Gitlab.config.lfs.enabled
+  end
+
   private
 
   def repository_storage_paths
diff --git a/app/models/note.rb b/app/models/note.rb
index ddcd7f9d034dd2421f7ff4ef53977d180f1ca7eb..2d644b03e4dbd4e993acc09b38e048409b44d28f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
   include Awardable
   include Importable
   include FasterCacheKeys
+  include CacheMarkdownField
+
+  cache_markdown_field :note, pipeline: :note
 
   # Attribute containing rendered and redacted Markdown as generated by
   # Banzai::ObjectRenderer.
-  attr_accessor :note_html
+  attr_accessor :redacted_note_html
 
   # An Array containing the number of visible references as generated by
   # Banzai::ObjectRenderer
@@ -25,6 +28,9 @@ class Note < ActiveRecord::Base
   belongs_to :author, class_name: "User"
   belongs_to :updated_by, class_name: "User"
 
+  # Only used by DiffNote, but defined here so that it can be used in `Note.includes`
+  belongs_to :resolved_by, class_name: "User"
+
   has_many :todos, dependent: :destroy
   has_many :events, as: :target, dependent: :destroy
 
@@ -59,7 +65,7 @@ class Note < ActiveRecord::Base
   scope :fresh, ->{ order(created_at: :asc, id: :asc) }
   scope :inc_author_project, ->{ includes(:project, :author) }
   scope :inc_author, ->{ includes(:author) }
-  scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) }
+  scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
 
   scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
   scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
@@ -70,7 +76,9 @@ class Note < ActiveRecord::Base
              project: [:project_members, { group: [:group_members] }])
   end
 
+  after_initialize :ensure_discussion_id
   before_validation :nullify_blank_type, :nullify_blank_line_code
+  before_validation :set_discussion_id
   after_save :keep_around_commit
 
   class << self
@@ -82,13 +90,18 @@ class Note < ActiveRecord::Base
       [:discussion, noteable_type.try(:underscore), noteable_id].join("-")
     end
 
+    def discussion_id(*args)
+      Digest::SHA1.hexdigest(build_discussion_id(*args))
+    end
+
     def discussions
       Discussion.for_notes(all)
     end
 
     def grouped_diff_discussions
-      notes = diff_notes.fresh.select(&:active?)
-      Discussion.for_diff_notes(notes).map { |d| [d.line_code, d] }.to_h
+      active_notes = diff_notes.fresh.select(&:active?)
+      Discussion.for_diff_notes(active_notes).
+        map { |d| [d.line_code, d] }.to_h
     end
 
     # Searches for notes matching the given query.
@@ -129,13 +142,16 @@ class Note < ActiveRecord::Base
     true
   end
 
-  def discussion_id
-    @discussion_id ||=
-      if for_merge_request?
-        [:discussion, :note, id].join("-")
-      else
-        self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
-      end
+  def resolvable?
+    false
+  end
+
+  def resolved?
+    false
+  end
+
+  def to_be_resolved?
+    resolvable? && !resolved?
   end
 
   def max_attachment_size
@@ -243,4 +259,28 @@ class Note < ActiveRecord::Base
   def nullify_blank_line_code
     self.line_code = nil if self.line_code.blank?
   end
+
+  def ensure_discussion_id
+    return unless self.persisted?
+    # Needed in case the SELECT statement doesn't ask for `discussion_id`
+    return unless self.has_attribute?(:discussion_id)
+    return if self.discussion_id
+
+    set_discussion_id
+    update_column(:discussion_id, self.discussion_id)
+  end
+
+  def set_discussion_id
+    self.discussion_id = Digest::SHA1.hexdigest(build_discussion_id)
+  end
+
+  def build_discussion_id
+    if for_merge_request?
+      # Notes on merge requests are always in a discussion of their own,
+      # so we generate a unique discussion ID.
+      [:discussion, :note, SecureRandom.hex].join("-")
+    else
+      self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
+    end
+  end
 end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 121b598b8f3839f67a9963bc31dfb3ad9b79f597..43fc218de2b5d458ab85add171d173ab8d2c0880 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
     :reopen_merge_request,
     :close_merge_request,
     :reassign_merge_request,
-    :merge_merge_request
+    :merge_merge_request,
+    :failed_pipeline,
+    :success_pipeline
   ]
 
   store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/project.rb b/app/models/project.rb
index eefdae3561531728b24fc36ba2a50a7a3e22a011..bbe590b5a8a0d122813bcebcb56c6150096c1a5c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -6,28 +6,38 @@ class Project < ActiveRecord::Base
   include Gitlab::VisibilityLevel
   include Gitlab::CurrentSettings
   include AccessRequestable
+  include CacheMarkdownField
   include Referable
   include Sortable
   include AfterCommitQueue
   include CaseSensitivity
   include TokenAuthenticatable
+  include ProjectFeaturesCompatibility
 
   extend Gitlab::ConfigHelper
 
+  class BoardLimitExceeded < StandardError; end
+
+  NUMBER_OF_PERMITTED_BOARDS = 1
   UNKNOWN_IMPORT_URL = 'http://unknown.git'
 
+  cache_markdown_field :description, pipeline: :description
+
+  delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+
   default_value_for :archived, false
   default_value_for :visibility_level, gitlab_config_features.visibility_level
+  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
+  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
+  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
   default_value_for :issues_enabled, gitlab_config_features.issues
   default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
   default_value_for :builds_enabled, gitlab_config_features.builds
   default_value_for :wiki_enabled, gitlab_config_features.wiki
   default_value_for :snippets_enabled, gitlab_config_features.snippets
-  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
-  default_value_for(:repository_storage) { current_application_settings.repository_storage }
-  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
 
   after_create :ensure_dir_exist
+  after_create :create_project_feature, unless: :project_feature
   after_save :ensure_dir_exist, if: :namespace_id_changed?
 
   # set last_activity_at to the same as created_at
@@ -58,11 +68,12 @@ class Project < ActiveRecord::Base
   alias_attribute :title, :name
 
   # Relations
-  belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
-  belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
+  belongs_to :creator, class_name: 'User'
+  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
   belongs_to :namespace
 
-  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
+  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
+  has_many :boards, before_add: :validate_board_limit, dependent: :destroy
 
   # Project services
   has_many :services
@@ -70,6 +81,7 @@ class Project < ActiveRecord::Base
   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
   has_one :hipchat_service, dependent: :destroy
@@ -100,7 +112,7 @@ class Project < ActiveRecord::Base
   # Merge requests from source project should be kept when source project was removed
   has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
   has_many :issues,             dependent: :destroy
-  has_many :labels,             dependent: :destroy
+  has_many :labels,             dependent: :destroy, class_name: 'ProjectLabel'
   has_many :services,           dependent: :destroy
   has_many :events,             dependent: :destroy
   has_many :milestones,         dependent: :destroy
@@ -109,7 +121,7 @@ class Project < ActiveRecord::Base
   has_many :hooks,              dependent: :destroy, class_name: 'ProjectHook'
   has_many :protected_branches, dependent: :destroy
 
-  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
   alias_method :members, :project_members
   has_many :users, through: :project_members
 
@@ -128,8 +140,9 @@ class Project < ActiveRecord::Base
   has_many :notification_settings, dependent: :destroy, as: :source
 
   has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
+  has_one :project_feature, dependent: :destroy
 
-  has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
+  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
@@ -140,9 +153,11 @@ class Project < ActiveRecord::Base
   has_many :deployments, dependent: :destroy
 
   accepts_nested_attributes_for :variables, allow_destroy: true
+  accepts_nested_attributes_for :project_feature
 
   delegate :name, to: :owner, allow_nil: true, prefix: true
   delegate :members, to: :team, prefix: true
+  delegate :add_user, to: :team
 
   # Validations
   validates :creator, presence: true, on: :create
@@ -157,8 +172,6 @@ class Project < ActiveRecord::Base
     length: { within: 0..255 },
     format: { with: Gitlab::Regex.project_path_regex,
               message: Gitlab::Regex.project_path_regex_message }
-  validates :issues_enabled, :merge_requests_enabled,
-            :wiki_enabled, inclusion: { in: [true, false] }
   validates :namespace, presence: true
   validates_uniqueness_of :name, scope: :namespace_id
   validates_uniqueness_of :path, scope: :namespace_id
@@ -194,6 +207,39 @@ class Project < ActiveRecord::Base
   scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
   scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
 
+  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+
+  # "enabled" here means "not disabled". It includes private features!
+  scope :with_feature_enabled, ->(feature) {
+    access_level_attribute = ProjectFeature.access_level_attribute(feature)
+    with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
+  }
+
+  # Picks a feature where the level is exactly that given.
+  scope :with_feature_access_level, ->(feature, level) {
+    access_level_attribute = ProjectFeature.access_level_attribute(feature)
+    with_project_feature.where(project_features: { access_level_attribute => level })
+  }
+
+  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
+  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+
+  # project features may be "disabled", "internal" or "enabled". If "internal",
+  # they are only available to team members. This scope returns projects where
+  # the feature is either enabled, or internal with permission for the user.
+  def self.with_feature_available_for_user(feature, user)
+    return with_feature_enabled(feature) if user.try(:admin?)
+
+    unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
+    return unconditional if user.nil?
+
+    conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
+    authorized = user.authorized_projects.merge(conditional.reorder(nil))
+
+    union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
+    where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
+  end
+
   scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
   scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
 
@@ -367,18 +413,9 @@ class Project < ActiveRecord::Base
       %r{(?<project>#{name_pattern}/#{name_pattern})}
     end
 
-    def trending(since = 1.month.ago)
-      # By counting in the JOIN we don't expose the GROUP BY to the outer query.
-      # This means that calls such as "any?" and "count" just return a number of
-      # the total count, instead of the counts grouped per project as a Hash.
-      join_body = "INNER JOIN (
-        SELECT project_id, COUNT(*) AS amount
-        FROM notes
-        WHERE created_at >= #{sanitize(since)}
-        GROUP BY project_id
-      ) join_note_counts ON projects.id = join_note_counts.project_id"
-
-      joins(join_body).reorder('join_note_counts.amount DESC')
+    def trending
+      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
+        reorder('trending_projects.id ASC')
     end
 
     def cached_count
@@ -386,6 +423,16 @@ class Project < ActiveRecord::Base
         Project.count
       end
     end
+
+    def group_ids
+      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
+    end
+  end
+
+  def lfs_enabled?
+    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
+
+    self[:lfs_enabled] && Gitlab.config.lfs.enabled
   end
 
   def repository_storage_path
@@ -434,7 +481,7 @@ class Project < ActiveRecord::Base
 
   # ref can't be HEAD, can only be branch/tag name or SHA
   def latest_successful_builds_for(ref = default_branch)
-    latest_pipeline = pipelines.latest_successful_for(ref).first
+    latest_pipeline = pipelines.latest_successful_for(ref)
 
     if latest_pipeline
       latest_pipeline.builds.latest.with_artifacts
@@ -469,8 +516,6 @@ class Project < ActiveRecord::Base
   end
 
   def reset_cache_and_import_attrs
-    update(import_error: nil)
-
     ProjectCacheWorker.perform_async(self.id)
 
     self.import_data.destroy if self.import_data
@@ -485,7 +530,7 @@ class Project < ActiveRecord::Base
   end
 
   def import_url
-    if import_data && super
+    if import_data && super.present?
       import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
       import_url.full_url
     else
@@ -609,10 +654,12 @@ class Project < ActiveRecord::Base
   end
 
   def new_issue_address(author)
-    if Gitlab::IncomingEmail.enabled? && author
-      Gitlab::IncomingEmail.reply_address(
-        "#{path_with_namespace}+#{author.authentication_token}")
-    end
+    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
+
+    author.ensure_incoming_email_token!
+
+    Gitlab::IncomingEmail.reply_address(
+      "#{path_with_namespace}+#{author.incoming_email_token}")
   end
 
   def build_commit_note(commit)
@@ -655,6 +702,10 @@ class Project < ActiveRecord::Base
     end
   end
 
+  def issue_reference_pattern
+    issues_tracker.reference_pattern
+  end
+
   def default_issues_tracker?
     !external_issue_tracker
   end
@@ -677,6 +728,10 @@ class Project < ActiveRecord::Base
     update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
   end
 
+  def has_wiki?
+    wiki_enabled? || has_external_wiki?
+  end
+
   def external_wiki
     if has_external_wiki.nil?
       cache_has_external_wiki # Populate
@@ -706,7 +761,7 @@ class Project < ActiveRecord::Base
 
         if template.nil?
           # If no template, we should create an instance. Ex `create_gitlab_ci_service`
-          self.send :"create_#{service_name}_service"
+          public_send("create_#{service_name}_service")
         else
           Service.create_from_template(self.id, template)
         end
@@ -716,10 +771,8 @@ class Project < ActiveRecord::Base
 
   def create_labels
     Label.templates.each do |label|
-      label = label.dup
-      label.template = nil
-      label.project_id = self.id
-      label.save
+      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
+      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
     end
   end
 
@@ -817,11 +870,6 @@ class Project < ActiveRecord::Base
     end
   end
 
-  def update_merge_requests(oldrev, newrev, ref, user)
-    MergeRequests::RefreshService.new(self, user).
-      execute(oldrev, newrev, ref)
-  end
-
   def valid_repo?
     repository.exists?
   rescue
@@ -1001,10 +1049,6 @@ class Project < ActiveRecord::Base
     project_members.find_by(user_id: user)
   end
 
-  def add_user(user, access_level, current_user = nil)
-    team.add_user(user, access_level, current_user)
-  end
-
   def default_branch
     @default_branch ||= repository.root_ref if repository.exists?
   end
@@ -1032,6 +1076,7 @@ class Project < ActiveRecord::Base
                                         "refs/heads/#{branch}",
                                         force: true)
     repository.copy_gitattributes(branch)
+    repository.expire_avatar_cache(branch)
     reload_default_branch
   end
 
@@ -1051,10 +1096,6 @@ class Project < ActiveRecord::Base
     forks.count
   end
 
-  def find_label(name)
-    labels.find_by(name: name)
-  end
-
   def origin_merge_requests
     merge_requests.where(source_project_id: self.id)
   end
@@ -1092,16 +1133,21 @@ class Project < ActiveRecord::Base
     !namespace.share_with_group_lock
   end
 
-  def pipeline(sha, ref)
+  def pipeline_for(ref, sha = nil)
+    sha ||= commit(ref).try(:sha)
+
+    return unless sha
+
     pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
   end
 
-  def ensure_pipeline(sha, ref, current_user = nil)
-    pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
+  def ensure_pipeline(ref, sha, current_user = nil)
+    pipeline_for(ref, sha) ||
+      pipelines.create(sha: sha, ref: ref, user: current_user)
   end
 
   def enable_ci
-    self.builds_enabled = true
+    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
   end
 
   def any_runners?(&block)
@@ -1116,12 +1162,6 @@ class Project < ActiveRecord::Base
     self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
   end
 
-  # TODO (ayufan): For now we use runners_token (backward compatibility)
-  # In 8.4 every build will have its own individual token valid for time of build
-  def valid_build_token?(token)
-    self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
-  end
-
   def build_coverage_enabled?
     build_coverage_regex.present?
   end
@@ -1266,8 +1306,40 @@ class Project < ActiveRecord::Base
     end
   end
 
+  def pushes_since_gc
+    Gitlab::Redis.with { |redis| redis.get(pushes_since_gc_redis_key).to_i }
+  end
+
+  def increment_pushes_since_gc
+    Gitlab::Redis.with { |redis| redis.incr(pushes_since_gc_redis_key) }
+  end
+
+  def reset_pushes_since_gc
+    Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
+  end
+
+  def environments_for(ref, commit, with_tags: false)
+    environment_ids = deployments.group(:environment_id).
+      select(:environment_id)
+
+    environment_ids =
+      if with_tags
+        environment_ids.where('ref=? OR tag IS TRUE', ref)
+      else
+        environment_ids.where(ref: ref)
+      end
+
+    environments.available.where(id: environment_ids).select do |environment|
+      environment.includes_commit?(commit)
+    end
+  end
+
   private
 
+  def pushes_since_gc_redis_key
+    "projects/#{id}/pushes_since_gc"
+  end
+
   def default_branch_protected?
     current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
       current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
@@ -1296,4 +1368,15 @@ class Project < ActiveRecord::Base
 
     shared_projects.any?
   end
+
+  # Similar to the normal callbacks that hook into the life cycle of an
+  # Active Record object, you can also define callbacks that get triggered
+  # when you add an object to an association collection. If any of these
+  # callbacks throw an exception, the object will not be added to the
+  # collection. Before you add a new board to the boards collection if you
+  # already have 1, 2, or n it will fail, but it if you have 0 that is lower
+  # than the number of permitted boards per project it won't fail.
+  def validate_board_limit(board)
+    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
+  end
 end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34fd5a57b5e9de0a09764b04493cbd0f42731879
--- /dev/null
+++ b/app/models/project_feature.rb
@@ -0,0 +1,95 @@
+class ProjectFeature < ActiveRecord::Base
+  # == Project features permissions
+  #
+  # Grants access level to project tools
+  #
+  # Tools can be enabled only for users, everyone or disabled
+  # Access control is made only for non private projects
+  #
+  # levels:
+  #
+  # Disabled: not enabled for anyone
+  # Private:  enabled only for team members
+  # Enabled:  enabled for everyone able to access the project
+  #
+
+  # Permission levels
+  DISABLED = 0
+  PRIVATE  = 10
+  ENABLED  = 20
+
+  FEATURES = %i(issues merge_requests wiki snippets builds repository)
+
+  class << self
+    def access_level_attribute(feature)
+      feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+      raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+      "#{feature}_access_level".to_sym
+    end
+  end
+
+  # Default scopes force us to unscope here since a service may need to check
+  # permissions for a project in pending_delete
+  # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
+  belongs_to :project, -> { unscope(where: :pending_delete) }
+
+  validate :repository_children_level
+
+  default_value_for :builds_access_level,         value: ENABLED, allows_nil: false
+  default_value_for :issues_access_level,         value: ENABLED, allows_nil: false
+  default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
+  default_value_for :snippets_access_level,       value: ENABLED, allows_nil: false
+  default_value_for :wiki_access_level,           value: ENABLED, allows_nil: false
+  default_value_for :repository_access_level,     value: ENABLED, allows_nil: false
+
+  def feature_available?(feature, user)
+    access_level = public_send(ProjectFeature.access_level_attribute(feature))
+    get_permission(user, access_level)
+  end
+
+  def builds_enabled?
+    return true unless builds_access_level
+
+    builds_access_level > DISABLED
+  end
+
+  def wiki_enabled?
+    return true unless wiki_access_level
+
+    wiki_access_level > DISABLED
+  end
+
+  def merge_requests_enabled?
+    return true unless merge_requests_access_level
+
+    merge_requests_access_level > DISABLED
+  end
+
+  private
+
+  # Validates builds and merge requests access level
+  # which cannot be higher than repository access level
+  def repository_children_level
+    validator = lambda do |field|
+      level = public_send(field) || ProjectFeature::ENABLED
+      not_allowed = level > repository_access_level
+      self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
+    end
+
+    %i(merge_requests_access_level builds_access_level).each(&validator)
+  end
+
+  def get_permission(user, level)
+    case level
+    when DISABLED
+      false
+    when PRIVATE
+      user && (project.team.member?(user) || user.admin?)
+    when ENABLED
+      true
+    else
+      true
+    end
+  end
+end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c8473b69054824db5724ae4d12d77954..db46def11eb2f33d9bb3f21b9817d4dd3d2f0eba 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
 class ProjectGroupLink < ActiveRecord::Base
+  include Expirable
+
   GUEST     = 10
   REPORTER  = 20
   DEVELOPER = 30
@@ -8,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
   belongs_to :group
 
   validates :project_id, presence: true
-  validates :group_id, presence: true
+  validates :group, presence: true
   validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
   validates :group_access, presence: true
   validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
     self.class.access_options.key(self.group_access)
   end
 
-  private 
+  private
 
   def different_group
     if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
new file mode 100644
index 0000000000000000000000000000000000000000..82f47f0e8fd407b96abb1320019fc8cb951c7ea1
--- /dev/null
+++ b/app/models/project_label.rb
@@ -0,0 +1,38 @@
+class ProjectLabel < Label
+  MAX_NUMBER_OF_PRIORITIES = 1
+
+  belongs_to :project
+
+  validates :project, presence: true
+
+  validate :permitted_numbers_of_priorities
+  validate :title_must_not_exist_at_group_level
+
+  delegate :group, to: :project, allow_nil: true
+
+  alias_attribute :subject, :project
+
+  def subject_foreign_key
+    'project_id'
+  end
+
+  def to_reference(target_project = nil, format: :id)
+    super(project, target_project, format: format)
+  end
+
+  private
+
+  def title_must_not_exist_at_group_level
+    return unless group.present? && title_changed?
+
+    if group.labels.with_title(self.title).exists?
+      errors.add(:title, :label_already_exists_at_group_level, group: group.name)
+    end
+  end
+
+  def permitted_numbers_of_priorities
+    if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
+      errors.add(:priorities, 'Number of permitted priorities exceeded')
+    end
+  end
+end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 81af55aa29adeb17b6edaf2bfed4624dbd8cba50..338e685339a03c1536fcdb9fa973ce52482238c7 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -1,4 +1,6 @@
 class BugzillaService < IssueTrackerService
+  validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
   prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
 
   def title
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index fa66e5864b8b002efd92d17aa737acc96a947a48..201b94b065ba899d607c4d9ce98782c413e72e30 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -43,7 +43,7 @@ class BuildsEmailService < Service
   end
 
   def can_test?
-    project.builds.count > 0
+    project.builds.any?
   end
 
   def disabled_title
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 63a5ed144841a16c8e6d185f2f08754d3418984b..b2f426dc2acc4e6bb42fd965491a18bcdbd6a7cb 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,4 +1,6 @@
 class CustomIssueTrackerService < IssueTrackerService
+  validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
   prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
 
   def title
@@ -9,6 +11,10 @@ class CustomIssueTrackerService < IssueTrackerService
     end
   end
 
+  def title=(value)
+    self.properties['title'] = value if self.properties
+  end
+
   def description
     if self.properties && self.properties['description'].present?
       self.properties['description']
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 5d17c3583306c552198b5a08c28c2b127d779a4f..6bd8d4ec5682fd9395989fd1cf76b338a4fe854e 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -1,6 +1,8 @@
 class GitlabIssueTrackerService < IssueTrackerService
   include Gitlab::Routing.url_helpers
 
+  validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
   prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
 
   default_value_for :default, true
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index d7c986c1a9112eb77590ae89b8e44bedd5088dc4..660a8ae3421ec13b447b270fea5f0184554bf964 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -1,5 +1,12 @@
 class HipchatService < Service
+  include ActionView::Helpers::SanitizeHelper
+
   MAX_COMMITS = 3
+  HIPCHAT_ALLOWED_TAGS = %w[
+    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
+  ]
 
   prop_accessor :token, :room, :server, :notify, :color, :api_version
   boolean_accessor :notify_only_broken_builds
@@ -39,7 +46,7 @@ class HipchatService < Service
   end
 
   def supported_events
-    %w(push issue merge_request note tag_push build)
+    %w(push issue confidential_issue merge_request note tag_push build)
   end
 
   def execute(data)
@@ -88,6 +95,10 @@ class HipchatService < Service
     end
   end
 
+  def render_line(text)
+    markdown(text.lines.first.chomp, pipeline: :single_line) if text
+  end
+
   def create_push_message(push)
     ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
     ref = Gitlab::Git.ref_name(push[:ref])
@@ -110,7 +121,7 @@ class HipchatService < Service
       message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
 
       push[:commits].take(MAX_COMMITS).each do |commit|
-        message << "<br /> - #{commit[:message].lines.first} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
+        message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
       end
 
       if push[:commits].count > MAX_COMMITS
@@ -121,12 +132,22 @@ class HipchatService < Service
     message
   end
 
-  def format_body(body)
-    if body
-      body = body.truncate(200, separator: ' ', omission: '...')
-    end
+  def markdown(text, options = {})
+    return "" unless text
+
+    context = {
+      project: project,
+      pipeline: :email
+    }
+
+    Banzai.render(text, context)
 
-    "<pre>#{body}</pre>"
+    context.merge!(options)
+
+    html = Banzai.post_process(Banzai.render(text, context), context)
+    sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
+
+    sanitized_html.truncate(200, separator: ' ', omission: '...')
   end
 
   def create_issue_message(data)
@@ -134,7 +155,7 @@ class HipchatService < Service
 
     obj_attr = data[:object_attributes]
     obj_attr = HashWithIndifferentAccess.new(obj_attr)
-    title = obj_attr[:title]
+    title = render_line(obj_attr[:title])
     state = obj_attr[:state]
     issue_iid = obj_attr[:iid]
     issue_url = obj_attr[:url]
@@ -143,10 +164,7 @@ class HipchatService < Service
     issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>"
     message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"
 
-    if description
-      description = format_body(description)
-      message << description
-    end
+    message << "<pre>#{markdown(description)}</pre>"
 
     message
   end
@@ -159,23 +177,20 @@ class HipchatService < Service
     merge_request_id = obj_attr[:iid]
     state = obj_attr[:state]
     description = obj_attr[:description]
-    title = obj_attr[:title]
+    title = render_line(obj_attr[:title])
 
     merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
     merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
     message = "#{user_name} #{state} #{merge_request_link} in " \
       "#{project_link}: <b>#{title}</b>"
 
-    if description
-      description = format_body(description)
-      message << description
-    end
+    message << "<pre>#{markdown(description)}</pre>"
 
     message
   end
 
   def format_title(title)
-    "<b>" + title.lines.first.chomp + "</b>"
+    "<b>#{render_line(title)}</b>"
   end
 
   def create_note_message(data)
@@ -186,11 +201,13 @@ class HipchatService < Service
     note = obj_attr[:note]
     note_url = obj_attr[:url]
     noteable_type = obj_attr[:noteable_type]
+    commit_id = nil
 
     case noteable_type
     when "Commit"
       commit_attr = HashWithIndifferentAccess.new(data[:commit])
-      subject_desc = commit_attr[:id]
+      commit_id = commit_attr[:id]
+      subject_desc = commit_id
       subject_desc = Commit.truncate_sha(subject_desc)
       subject_type = "commit"
       title = format_title(commit_attr[:message])
@@ -218,10 +235,7 @@ class HipchatService < Service
     message = "#{user_name} commented on #{subject_html} in #{project_link}: "
     message << title
 
-    if note
-      note = format_body(note)
-      message << note
-    end
+    message << "<pre>#{markdown(note, ref: commit_id)}</pre>"
 
     message
   end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index d1df6d0292f84c0da475268a696d665d469a3f39..207bb816ad152b538729bb8fd651193048846708 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,8 +1,12 @@
 class IssueTrackerService < Service
-  validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
-
   default_value_for :category, 'issue_tracker'
 
+  # Pattern used to extract links from comments
+  # Override this method on services that uses different patterns
+  def reference_pattern
+    @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+  end
+
   def default?
     default
   end
@@ -32,18 +36,24 @@ class IssueTrackerService < Service
     ]
   end
 
-  def initialize_properties
-    if properties.nil?
-      if enabled_in_gitlab_config
+  # Initialize with default properties values
+  # or receive a block with custom properties
+  def initialize_properties(&block)
+    return unless properties.nil?
+
+    if enabled_in_gitlab_config
+      if block_given?
+        yield
+      else
         self.properties = {
           title: issues_tracker['title'],
           project_url: issues_tracker['project_url'],
           issues_url: issues_tracker['issues_url'],
           new_issue_url: issues_tracker['new_issue_url']
         }
-      else
-        self.properties = {}
       end
+    else
+      self.properties = {}
     end
   end
 
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 97bcbacf2b942f025ba1a4d4df5a245157716729..2dbe007546564704cac3ea72c4c62029e29f6803 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,28 +1,80 @@
+# == Schema Information
+#
+# Table name: services
+#
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
+#  build_events          :boolean          default(FALSE), not null
+#
+
 class JiraService < IssueTrackerService
-  include HTTParty
   include Gitlab::Routing.url_helpers
 
-  DEFAULT_API_VERSION = 2
+  validates :url, url: true, presence: true, if: :activated?
+  validates :project_key, presence: true, if: :activated?
 
-  prop_accessor :username, :password, :api_url, :jira_issue_transition_id,
-                :title, :description, :project_url, :issues_url, :new_issue_url
+  prop_accessor :username, :password, :url, :project_key,
+                :jira_issue_transition_id, :title, :description
 
-  validates :api_url, presence: true, url: true, if: :activated?
+  before_update :reset_password
 
-  before_validation :set_api_url, :set_jira_issue_transition_id
+  # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
+  def reference_pattern
+    @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+  end
 
-  before_update :reset_password
+  def initialize_properties
+    super do
+      self.properties = {
+        title: issues_tracker['title'],
+        url: issues_tracker['url']
+      }
+    end
+  end
 
   def reset_password
     # don't reset the password if a new one is provided
-    if api_url_changed? && !password_touched?
+    if url_changed? && !password_touched?
       self.password = nil
     end
   end
 
+  def options
+    url = URI.parse(self.url)
+
+    {
+      username: self.username,
+      password: self.password,
+      site: URI.join(url, '/').to_s,
+      context_path: url.path,
+      auth_type: :basic,
+      read_timeout: 120,
+      use_ssl: url.scheme == 'https'
+    }
+  end
+
+  def client
+    @client ||= JIRA::Client.new(options)
+  end
+
+  def jira_project
+    @jira_project ||= client.Project.find(project_key)
+  end
+
   def help
-    'Setting `project_url`, `issues_url` and `new_issue_url` will '\
-    'allow a user to easily navigate to the Jira issue tracker. See the '\
+    'See the ' \
     '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
     'for details.'
   end
@@ -48,12 +100,26 @@ class JiraService < IssueTrackerService
   end
 
   def fields
-    super.push(
-      { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' },
+    [
+      { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
+      { type: 'text', name: 'project_key', placeholder: 'Project Key' },
       { type: 'text', name: 'username', placeholder: '' },
       { type: 'password', name: 'password', placeholder: '' },
       { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
-    )
+    ]
+  end
+
+  # URLs to redirect from Gitlab issues pages to jira issue tracker
+  def project_url
+    "#{url}/issues/?jql=project=#{project_key}"
+  end
+
+  def issues_url
+    "#{url}/browse/:id"
+  end
+
+  def new_issue_url
+    "#{url}/secure/CreateIssue.jspa"
   end
 
   def execute(push, issue = nil)
@@ -67,7 +133,7 @@ class JiraService < IssueTrackerService
   end
 
   def create_cross_reference_note(mentioned, noteable, author)
-    issue_name = mentioned.id
+    issue_key = mentioned.id
     project = self.project
     noteable_name = noteable.class.name.underscore.downcase
     noteable_id = if noteable.is_a?(Commit)
@@ -94,58 +160,43 @@ class JiraService < IssueTrackerService
       }
     }
 
-    add_comment(data, issue_name)
+    add_comment(data, issue_key)
   end
 
-  def test_settings
-    return unless api_url.present?
-    result = JiraService.get(
-      jira_api_test_url,
-      headers: {
-        'Content-Type' => 'application/json',
-        'Authorization' => "Basic #{auth}"
-      }
-    )
-
-    case result.code
-    when 201, 200
-      Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.")
-      true
-    else
-      Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}")
-      false
-    end
-  rescue Errno::ECONNREFUSED => e
-    Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}."
-    false
+  # reason why service cannot be tested
+  def disabled_title
+    "Please fill in Password and Username."
   end
 
-  private
-
-  def build_api_url_from_project_url
-    server = URI(project_url)
-    default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port])
-    server_url = "#{server.scheme}://#{server.host}"
-    server_url.concat(":#{server.port}") unless default_ports
-    "#{server_url}/rest/api/#{DEFAULT_API_VERSION}"
-  rescue
-    "" # looks like project URL was not valid
+  def can_test?
+    username.present? && password.present?
   end
 
-  def set_api_url
-    self.api_url = build_api_url_from_project_url if self.api_url.blank?
+  # JIRA does not need test data.
+  # We are requesting the project that belongs to the project key.
+  def test_data(user = nil, project = nil)
+    nil
   end
 
-  def set_jira_issue_transition_id
-    self.jira_issue_transition_id ||= "2"
+  def test_settings
+    return unless url.present?
+    # Test settings by getting the project
+    jira_project
+
+  rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
+    Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
+    false
   end
 
+  private
+
   def close_issue(entity, issue)
     commit_id = if entity.is_a?(Commit)
                   entity.id
                 elsif entity.is_a?(MergeRequest)
                   entity.diff_head_sha
                 end
+
     commit_url = build_entity_url(:commit, commit_id)
 
     # Depending on the JIRA project's workflow, a comment during transition
@@ -156,24 +207,16 @@ class JiraService < IssueTrackerService
   end
 
   def transition_issue(issue)
-    message = {
-      transition: {
-        id: jira_issue_transition_id
-      }
-    }
-    send_message(close_issue_url(issue.iid), message.to_json)
+    issue = client.Issue.find(issue.iid)
+    issue.transitions.build.save(transition: { id: jira_issue_transition_id })
   end
 
   def add_issue_solved_comment(issue, commit_id, commit_url)
-    comment = {
-      body: "Issue solved with [#{commit_id}|#{commit_url}]."
-    }
-
-    send_message(comment_url(issue.iid), comment.to_json)
+    comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+    send_message(issue.iid, comment)
   end
 
-  def add_comment(data, issue_name)
-    url = comment_url(issue_name)
+  def add_comment(data, issue_key)
     user_name = data[:user][:name]
     user_url = data[:user][:url]
     entity_name = data[:entity][:name]
@@ -181,72 +224,35 @@ class JiraService < IssueTrackerService
     entity_title = data[:entity][:title]
     project_name = data[:project][:name]
 
-    message = {
-      body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'}
-    }
+    message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
 
-    unless existing_comment?(issue_name, message[:body])
-      send_message(url, message.to_json)
+    unless comment_exists?(issue_key, message)
+      send_message(issue_key, message)
     end
   end
 
-  def auth
-    require 'base64'
-    Base64.urlsafe_encode64("#{self.username}:#{self.password}")
-  end
-
-  def send_message(url, message)
-    return unless api_url.present?
-    result = JiraService.post(
-      url,
-      body: message,
-      headers: {
-        'Content-Type' => 'application/json',
-        'Authorization' => "Basic #{auth}"
-      }
-    )
-
-    message = case result.code
-              when 201, 200, 204
-                "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}."
-              when 401
-                "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again."
-              else
-                "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}"
-              end
-
-    Rails.logger.info(message)
-    message
-  rescue URI::InvalidURIError, Errno::ECONNREFUSED => e
-    Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}."
+  def comment_exists?(issue_key, message)
+    comments = client.Issue.find(issue_key).comments
+    comments.map { |comment| comment.body.include?(message) }.any?
   end
 
-  def existing_comment?(issue_name, new_comment)
-    return unless api_url.present?
-    result = JiraService.get(
-      comment_url(issue_name),
-      headers: {
-        'Content-Type' => 'application/json',
-        'Authorization' => "Basic #{auth}"
-      }
-    )
+  def send_message(issue_key, message)
+    return unless url.present?
 
-    case result.code
-    when 201, 200
-      existing_comments = JSON.parse(result.body)['comments']
+    issue = client.Issue.find(issue_key)
 
-      if existing_comments.present?
-        return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any?
-      end
+    if issue.comments.build.save!(body: message)
+      result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
     end
 
-    false
-  rescue JSON::ParserError
-    false
+    Rails.logger.info(result_message)
+    result_message
+  rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
+    Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
   end
 
   def resource_url(resource)
-    "#{Settings.gitlab['url'].chomp("/")}#{resource}"
+    "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
   end
 
   def build_entity_url(entity_name, entity_id)
@@ -262,16 +268,4 @@ class JiraService < IssueTrackerService
       )
     )
   end
-
-  def close_issue_url(issue_name)
-    "#{self.api_url}/issue/#{issue_name}/transitions"
-  end
-
-  def comment_url(issue_name)
-    "#{self.api_url}/issue/#{issue_name}/comment"
-  end
-
-  def jira_api_test_url
-    "#{self.api_url}/myself"
-  end
 end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..745f9bd1b43f492f9d1c3fb999e901ba7dc73d84
--- /dev/null
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -0,0 +1,84 @@
+class PipelinesEmailService < Service
+  prop_accessor :recipients
+  boolean_accessor :notify_only_broken_pipelines
+  validates :recipients, presence: true, if: :activated?
+
+  def initialize_properties
+    self.properties ||= { notify_only_broken_pipelines: true }
+  end
+
+  def title
+    'Pipelines emails'
+  end
+
+  def description
+    'Email the pipelines status to a list of recipients.'
+  end
+
+  def to_param
+    'pipelines_email'
+  end
+
+  def supported_events
+    %w[pipeline]
+  end
+
+  def execute(data, force: false)
+    return unless supported_events.include?(data[:object_kind])
+    return unless force || should_pipeline_be_notified?(data)
+
+    all_recipients = retrieve_recipients(data)
+
+    return unless all_recipients.any?
+
+    pipeline_id = data[:object_attributes][:id]
+    PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
+  end
+
+  def can_test?
+    project.pipelines.any?
+  end
+
+  def disabled_title
+    'Please setup a pipeline on your repository.'
+  end
+
+  def test_data(project, user)
+    data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+    data[:user] = user.hook_attrs
+    data
+  end
+
+  def fields
+    [
+      { type: 'textarea',
+        name: 'recipients',
+        placeholder: 'Emails separated by comma' },
+      { type: 'checkbox',
+        name: 'notify_only_broken_pipelines' },
+    ]
+  end
+
+  def test(data)
+    result = execute(data, force: true)
+
+    { success: true, result: result }
+  rescue StandardError => error
+    { success: false, result: error }
+  end
+
+  def should_pipeline_be_notified?(data)
+    case data[:object_attributes][:status]
+    when 'success'
+      !notify_only_broken_pipelines?
+    when 'failed'
+      true
+    else
+      false
+    end
+  end
+
+  def retrieve_recipients(data)
+    recipients.to_s.split(',').reject(&:blank?)
+  end
+end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index f634e0772c0480d1dff2e925b167c8ef650e0fb1..f9da273cf0849ea24bb423080bf861cdb4e8f27e 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -1,4 +1,6 @@
 class RedmineService < IssueTrackerService
+  validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
   prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
 
   def title
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index abbc780dc1a09fe6e45ea29102b2ea93b5049539..e1b937817f4649d7bfd4adbdd10665da35b023ef 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,6 +1,6 @@
 class SlackService < Service
   prop_accessor :webhook, :username, :channel
-  boolean_accessor :notify_only_broken_builds
+  boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
   validates :webhook, presence: true, url: true, if: :activated?
 
   def initialize_properties
@@ -10,6 +10,7 @@ class SlackService < Service
     if properties.nil?
       self.properties = {}
       self.notify_only_broken_builds = true
+      self.notify_only_broken_pipelines = true
     end
   end
 
@@ -38,13 +39,15 @@ class SlackService < Service
         { type: 'text', name: 'username', placeholder: 'username' },
         { type: 'text', name: 'channel', placeholder: "#general" },
         { type: 'checkbox', name: 'notify_only_broken_builds' },
+        { type: 'checkbox', name: 'notify_only_broken_pipelines' },
       ]
 
     default_fields + build_event_channels
   end
 
   def supported_events
-    %w(push issue merge_request note tag_push build wiki_page)
+    %w[push issue confidential_issue merge_request note tag_push
+       build pipeline wiki_page]
   end
 
   def execute(data)
@@ -62,32 +65,22 @@ class SlackService < Service
     # 'close' action. Ignore update events for now to prevent duplicate
     # messages from arriving.
 
-    message = \
-      case object_kind
-      when "push", "tag_push"
-        PushMessage.new(data)
-      when "issue"
-        IssueMessage.new(data) unless is_update?(data)
-      when "merge_request"
-        MergeMessage.new(data) unless is_update?(data)
-      when "note"
-        NoteMessage.new(data)
-      when "build"
-        BuildMessage.new(data) if should_build_be_notified?(data)
-      when "wiki_page"
-        WikiPageMessage.new(data)
-      end
-
-    opt = {}
-
-    event_channel = get_channel_field(object_kind) || channel
-
-    opt[:channel] = event_channel if event_channel
-    opt[:username] = username if username
+    message = get_message(object_kind, data)
 
     if message
+      opt = {}
+
+      event_channel = get_channel_field(object_kind) || channel
+
+      opt[:channel] = event_channel if event_channel
+      opt[:username] = username if username
+
       notifier = Slack::Notifier.new(webhook, opt)
       notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+
+      true
+    else
+      false
     end
   end
 
@@ -105,6 +98,25 @@ class SlackService < Service
 
   private
 
+  def get_message(object_kind, data)
+    case object_kind
+    when "push", "tag_push"
+      PushMessage.new(data)
+    when "issue"
+      IssueMessage.new(data) unless is_update?(data)
+    when "merge_request"
+      MergeMessage.new(data) unless is_update?(data)
+    when "note"
+      NoteMessage.new(data)
+    when "build"
+      BuildMessage.new(data) if should_build_be_notified?(data)
+    when "pipeline"
+      PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+    when "wiki_page"
+      WikiPageMessage.new(data)
+    end
+  end
+
   def get_channel_field(event)
     field_name = event_channel_name(event)
     self.public_send(field_name)
@@ -142,6 +154,17 @@ class SlackService < Service
       false
     end
   end
+
+  def should_pipeline_be_notified?(data)
+    case data[:object_attributes][:status]
+    when 'success'
+      !notify_only_broken_pipelines?
+    when 'failed'
+      true
+    else
+      false
+    end
+  end
 end
 
 require "slack_service/issue_message"
@@ -149,4 +172,5 @@ require "slack_service/push_message"
 require "slack_service/merge_message"
 require "slack_service/note_message"
 require "slack_service/build_message"
+require "slack_service/pipeline_message"
 require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
index 69c21b3fc387fdf4ecbf98773ebf468ed8812f87..0fca4267bad8eb3f9246c40cf09e24a823526d0a 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/slack_service/build_message.rb
@@ -9,7 +9,7 @@ class SlackService
     attr_reader :user_name
     attr_reader :duration
 
-    def initialize(params, commit = true)
+    def initialize(params)
       @sha = params[:sha]
       @ref_type = params[:tag] ? 'tag' : 'branch'
       @ref = params[:ref]
@@ -36,7 +36,7 @@ class SlackService
 
     def message
       "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
-    end   
+    end
 
     def format(string)
       Slack::Notifier::LinkFormatter.format(string)
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
index 88e053ec19274d5ecbfcaf9fc13a78bbd145566a..cd87a79d0c607383f6c92b0c66cda190f095dcd4 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/slack_service/issue_message.rb
@@ -11,7 +11,7 @@ class SlackService
     attr_reader :description
 
     def initialize(params)
-      @user_name = params[:user][:name]
+      @user_name = params[:user][:username]
       @project_name = params[:project_name]
       @project_url = params[:project_url]
 
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
index 11fc691022bbfe23fc85ef05cd75c242f9891f67..b7615c960686177ef046d6b1ea25e46b6be15c8b 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/slack_service/merge_message.rb
@@ -10,7 +10,7 @@ class SlackService
     attr_reader :title
 
     def initialize(params)
-      @user_name = params[:user][:name]
+      @user_name = params[:user][:username]
       @project_name = params[:project_name]
       @project_url = params[:project_url]
 
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 89ba51cb6627ee3d3b48fb00363e0e99c3a60a31..9e84e90f38c8b633003c61d412345430c95d2afa 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -10,7 +10,7 @@ class SlackService
 
     def initialize(params)
       params = HashWithIndifferentAccess.new(params)
-      @user_name = params[:user][:name]
+      @user_name = params[:user][:username]
       @project_name = params[:project_name]
       @project_url = params[:project_url]
 
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f06b3562965292b549c0781dc5467c65df282455
--- /dev/null
+++ b/app/models/project_services/slack_service/pipeline_message.rb
@@ -0,0 +1,79 @@
+class SlackService
+  class PipelineMessage < BaseMessage
+    attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+                :user_name, :duration, :pipeline_id
+
+    def initialize(data)
+      pipeline_attributes = data[:object_attributes]
+      @sha = pipeline_attributes[:sha]
+      @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+      @ref = pipeline_attributes[:ref]
+      @status = pipeline_attributes[:status]
+      @duration = pipeline_attributes[:duration]
+      @pipeline_id = pipeline_attributes[:id]
+
+      @project_name = data[:project][:path_with_namespace]
+      @project_url = data[:project][:web_url]
+      @user_name = data[:commit] && data[:commit][:author_name]
+    end
+
+    def pretext
+      ''
+    end
+
+    def fallback
+      format(message)
+    end
+
+    def attachments
+      [{ text: format(message), color: attachment_color }]
+    end
+
+    private
+
+    def message
+      "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+    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
+      "[#{ref}](#{branch_url})"
+    end
+
+    def project_link
+      "[#{project_name}](#{project_url})"
+    end
+
+    def pipeline_url
+      "#{project_url}/pipelines/#{pipeline_id}"
+    end
+
+    def pipeline_link
+      "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+    end
+  end
+end
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
index f336d9e7691d56fdc2109823c91c41c60590f895..160ca3ac11523fa32fe5c5a36f640e1050401c11 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/slack_service/wiki_page_message.rb
@@ -9,7 +9,7 @@ class SlackService
     attr_reader :description
 
     def initialize(params)
-      @user_name = params[:user][:name]
+      @user_name = params[:user][:username]
       @project_name = params[:project_name]
       @project_url = params[:project_url]
 
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d0a714cd6fcbefcd5f00ef0339faa9af620791de..a6e911df9bd3d625996bf0534d9651b353d76ff5 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
     users, access, current_user = *args
 
     if users.respond_to?(:each)
-      add_users(users, access, current_user)
+      add_users(users, access, current_user: current_user)
     else
-      add_user(users, access, current_user)
+      add_user(users, access, current_user: current_user)
     end
   end
 
@@ -33,17 +33,24 @@ class ProjectTeam
     member
   end
 
-  def add_users(users, access, current_user = nil)
+  def add_users(users, access_level, current_user: nil, expires_at: nil)
     ProjectMember.add_users_to_projects(
       [project.id],
       users,
-      access,
-      current_user
+      access_level,
+      current_user: current_user,
+      expires_at: expires_at
     )
   end
 
-  def add_user(user, access, current_user = nil)
-    add_users([user], access, current_user)
+  def add_user(user, access_level, current_user: nil, expires_at: nil)
+    ProjectMember.add_user(
+      project,
+      user,
+      access_level,
+      current_user: current_user,
+      expires_at: expires_at
+    )
   end
 
   # Remove all users from project team
@@ -118,14 +125,8 @@ class ProjectTeam
     max_member_access(user.id) == Gitlab::Access::MASTER
   end
 
-  def member?(user, min_member_access = nil)
-    member = !!find_member(user.id)
-
-    if min_member_access
-      member && max_member_access(user.id) >= min_member_access
-    else
-      member
-    end
+  def member?(user, min_member_access = Gitlab::Access::GUEST)
+    max_member_access(user.id) >= min_member_access
   end
 
   def human_max_access(user_id)
@@ -162,7 +163,7 @@ class ProjectTeam
 
       # Each group produces a list of maximum access level per user. We take the
       # max of the values produced by each group.
-      if project.invited_groups.any? && project.allowed_to_share_with_group?
+      if project_shared_with_group?
         project.project_group_links.each do |group_link|
           invited_access = max_invited_level_for_users(group_link, user_ids)
           merge_max!(access, invited_access)
@@ -199,43 +200,17 @@ class ProjectTeam
   def fetch_members(level = nil)
     project_members = project.members
     group_members = group ? group.members : []
-    invited_members = []
-
-    if project.invited_groups.any? && project.allowed_to_share_with_group?
-      project.project_group_links.includes(group: [:group_members]).each do |group_link|
-        invited_group = group_link.group
-        im = invited_group.members
-
-        if level
-          int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
-          # Skip group members if we ask for masters
-          # but max group access is developers
-          next if int_level > group_link.group_access
-
-          # If we ask for developers and max
-          # group access is developers we need to provide
-          # both group master, developers as devs
-          if int_level == group_link.group_access
-            im.where("access_level >= ?)", group_link.group_access)
-          else
-            im.send(level)
-          end
-        end
-
-        invited_members << im
-      end
-
-      invited_members = invited_members.flatten.compact
-    end
 
     if level
-      project_members = project_members.send(level)
-      group_members = group_members.send(level) if group
+      project_members = project_members.public_send(level)
+      group_members = group_members.public_send(level) if group
     end
 
     user_ids = project_members.pluck(:user_id)
+
+    invited_members = fetch_invited_members(level)
     user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+
     user_ids.push(*group_members.pluck(:user_id)) if group
 
     User.where(id: user_ids)
@@ -248,4 +223,38 @@ class ProjectTeam
   def merge_max!(first_hash, second_hash)
     first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
   end
+
+  def project_shared_with_group?
+    project.invited_groups.any? && project.allowed_to_share_with_group?
+  end
+
+  def fetch_invited_members(level = nil)
+    invited_members = []
+
+    return invited_members unless project_shared_with_group?
+
+    project.project_group_links.includes(group: [:group_members]).each do |link|
+      invited_group_members = link.group.members
+
+      if level
+        numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+        # If we're asked for a level that's higher than the group's access,
+        # there's nothing left to do
+        next if numeric_level > link.group_access
+
+        # Make sure we include everyone _above_ the requested level as well
+        invited_group_members =
+          if numeric_level == link.group_access
+            invited_group_members.where("access_level >= ?", link.group_access)
+          else
+            invited_group_members.public_send(level)
+          end
+      end
+
+      invited_members << invited_group_members
+    end
+
+    invited_members.flatten.compact
+  end
 end
diff --git a/app/models/release.rb b/app/models/release.rb
index e196b84eb18845cbbfdf8b2acedd6309ab3931f2..c936899799e790d54163db02d94bc9c131cb2f0e 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,4 +1,8 @@
 class Release < ActiveRecord::Base
+  include CacheMarkdownField
+
+  cache_markdown_field :description
+
   belongs_to :project
 
   validates :description, :project, :tag, presence: true
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e56bac509a4ed1e774778c68b998565b8b1218b9..fe99190460129f03094e9b69978e37cd8a709a92 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -11,6 +11,20 @@ class Repository
 
   attr_accessor :path_with_namespace, :project
 
+  def self.storages
+    Gitlab.config.repositories.storages
+  end
+
+  def self.remove_storage_from_path(repo_path)
+    storages.find do |_, storage_path|
+      if repo_path.start_with?(storage_path)
+        return repo_path.sub(storage_path, '')
+      end
+    end
+
+    repo_path
+  end
+
   def initialize(path_with_namespace, project)
     @path_with_namespace = path_with_namespace
     @project = project
@@ -70,15 +84,17 @@ class Repository
 
   def commit(ref = 'HEAD')
     return nil unless exists?
+
     commit =
       if ref.is_a?(Gitlab::Git::Commit)
         ref
       else
         Gitlab::Git::Commit.find(raw_repository, ref)
       end
+
     commit = ::Commit.new(commit, @project) if commit
     commit
-  rescue Rugged::OdbError
+  rescue Rugged::OdbError, Rugged::TreeError
     nil
   end
 
@@ -109,19 +125,37 @@ class Repository
   end
 
   def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
+    unless exists? && has_visible_content? && query.present?
+      return []
+    end
+
     ref ||= root_ref
 
-    # Limited to 1000 commits for now, could be parameterized?
-    args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+    args = %W(
+      #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
+      --max-count #{limit} --grep=#{query} --regexp-ignore-case
+    )
     args = args.concat(%W(-- #{path})) if path.present?
 
-    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
-    commits = git_log_results.map { |c| commit(c) }
-    commits
+    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
+    git_log_results.map { |c| commit(c.chomp) }.compact
   end
 
-  def find_branch(name)
-    raw_repository.branches.find { |branch| branch.name == name }
+  def find_branch(name, fresh_repo: true)
+    # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
+    # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
+    # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
+    # may cause the branch to "disappear" erroneously or have the wrong SHA.
+    #
+    # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
+    raw_repo =
+      if fresh_repo
+        Gitlab::Git::Repository.new(path_to_repo)
+      else
+        raw_repository
+      end
+
+    raw_repo.find_branch(name)
   end
 
   def find_tag(name)
@@ -136,7 +170,7 @@ class Repository
     return false unless target
 
     GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
-      rugged.branches.create(branch_name, target)
+      update_ref!(ref, target, oldrev)
     end
 
     after_create_branch
@@ -163,12 +197,12 @@ class Repository
     before_remove_branch
 
     branch = find_branch(branch_name)
-    oldrev = branch.try(:target).try(:id)
+    oldrev = branch.try(:dereferenced_target).try(:id)
     newrev = Gitlab::Git::BLANK_SHA
     ref    = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
 
     GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
-      rugged.branches.delete(branch_name)
+      update_ref!(ref, newrev, oldrev)
     end
 
     after_remove_branch
@@ -200,6 +234,23 @@ class Repository
 
   def ref_exists?(ref)
     rugged.references.exist?(ref)
+  rescue Rugged::ReferenceError
+    false
+  end
+
+  def update_ref!(name, newrev, oldrev)
+    # We use 'git update-ref' because libgit2/rugged currently does not
+    # offer 'compare and swap' ref updates. Without compare-and-swap we can
+    # (and have!) accidentally reset the ref to an earlier state, clobbering
+    # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+    command = %w[git update-ref --stdin -z]
+    _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
+      stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
+    end
+
+    return if status.zero?
+
+    raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
   end
 
   # Makes sure a commit is kept around when Git garbage collection runs.
@@ -223,11 +274,7 @@ class Repository
   end
 
   def kept_around?(sha)
-    begin
-      ref_exists?(keep_around_ref_name(sha))
-    rescue Rugged::ReferenceError
-      false
-    end
+    ref_exists?(keep_around_ref_name(sha))
   end
 
   def tag_names
@@ -264,10 +311,10 @@ class Repository
       # Rugged seems to throw a `ReferenceError` when given branch_names rather
       # than SHA-1 hashes
       number_commits_behind = raw_repository.
-        count_commits_between(branch.target.sha, root_ref_hash)
+        count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
 
       number_commits_ahead = raw_repository.
-        count_commits_between(root_ref_hash, branch.target.sha)
+        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
 
       { behind: number_commits_behind, ahead: number_commits_ahead }
     end
@@ -277,7 +324,7 @@ class Repository
   def cache_keys
     %i(size commit_count
        readme version contribution_guide changelog
-       license_blob license_key gitignore)
+       license_blob license_key gitignore koding_yml)
   end
 
   # Keys for data on branch/tag operations.
@@ -386,11 +433,24 @@ class Repository
     @exists = nil
   end
 
+  # expire cache that doesn't depend on repository data (when expiring)
+  def expire_content_cache
+    expire_tags_cache
+    expire_tag_count_cache
+    expire_branches_cache
+    expire_branch_count_cache
+    expire_root_ref_cache
+    expire_emptiness_caches
+    expire_exists_cache
+  end
+
   # Runs code after a repository has been created.
   def after_create
     expire_exists_cache
     expire_root_ref_cache
     expire_emptiness_caches
+
+    repository_event(:create_repository)
   end
 
   # Runs code just before a repository is deleted.
@@ -399,14 +459,9 @@ class Repository
 
     expire_cache if exists?
 
-    # expire cache that don't depend on repository data (when expiring)
-    expire_tags_cache
-    expire_tag_count_cache
-    expire_branches_cache
-    expire_branch_count_cache
-    expire_root_ref_cache
-    expire_emptiness_caches
-    expire_exists_cache
+    expire_content_cache
+
+    repository_event(:remove_repository)
   end
 
   # Runs code just before the HEAD of a repository is changed.
@@ -414,6 +469,8 @@ class Repository
     # Cached divergent commit counts are based on repository head
     expire_branch_cache
     expire_root_ref_cache
+
+    repository_event(:change_default_branch)
   end
 
   # Runs code before pushing (= creating or removing) a tag.
@@ -421,28 +478,33 @@ class Repository
     expire_cache
     expire_tags_cache
     expire_tag_count_cache
+
+    repository_event(:push_tag)
   end
 
   # Runs code before removing a tag.
   def before_remove_tag
     expire_tags_cache
     expire_tag_count_cache
+
+    repository_event(:remove_tag)
   end
 
   def before_import
-    expire_emptiness_caches
-    expire_exists_cache
+    expire_content_cache
   end
 
   # Runs code after a repository has been forked/imported.
   def after_import
-    expire_emptiness_caches
-    expire_exists_cache
+    expire_content_cache
+    build_cache
   end
 
   # Runs code after a new commit has been pushed.
   def after_push_commit(branch_name, revision)
     expire_cache(branch_name, revision)
+
+    repository_event(:push_commit, branch: branch_name)
   end
 
   # Runs code after a new branch has been created.
@@ -450,11 +512,15 @@ class Repository
     expire_branches_cache
     expire_has_visible_content_cache
     expire_branch_count_cache
+
+    repository_event(:push_branch)
   end
 
   # Runs code before removing an existing branch.
   def before_remove_branch
     expire_branches_cache
+
+    repository_event(:remove_branch)
   end
 
   # Runs code after an existing branch has been removed.
@@ -537,6 +603,14 @@ class Repository
     end
   end
 
+  def koding_yml
+    return nil unless head_exists?
+
+    cache.fetch(:koding_yml) do
+      file_on_head(/\A\.koding\.yml\z/)
+    end
+  end
+
   def gitlab_ci_yml
     return nil unless head_exists?
 
@@ -622,11 +696,11 @@ class Repository
       branches.sort_by(&:name)
     when 'updated_desc'
       branches.sort do |a, b|
-        commit(b.target).committed_date <=> commit(a.target).committed_date
+        commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
       end
     when 'updated_asc'
       branches.sort do |a, b|
-        commit(a.target).committed_date <=> commit(b.target).committed_date
+        commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
       end
     else
       branches
@@ -665,6 +739,14 @@ class Repository
     end
   end
 
+  def ref_name_for_sha(ref_path, sha)
+    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
+
+    # Not found -> ["", 0]
+    # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
+    Gitlab::Popen.popen(args, path_to_repo).first.split.last
+  end
+
   def refs_contains_sha(ref_type, sha)
     args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
     names = Gitlab::Popen.popen(args, path_to_repo).first
@@ -704,64 +786,61 @@ class Repository
     @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
   end
 
-  def commit_dir(user, path, message, branch)
-    commit_with_hooks(user, branch) do |ref|
-      committer = user_to_committer(user)
-      options = {}
-      options[:committer] = committer
-      options[:author] = committer
-
-      options[:commit] = {
-        message: message,
-        branch: ref,
-        update_ref: false,
+  def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
+    update_branch_with_hooks(user, branch) do |ref|
+      options = {
+        commit: {
+          branch: ref,
+          message: message,
+          update_ref: false
+        }
       }
 
+      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
       raw_repository.mkdir(path, options)
     end
   end
 
-  def commit_file(user, path, content, message, branch, update)
-    commit_with_hooks(user, branch) do |ref|
-      committer = user_to_committer(user)
-      options = {}
-      options[:committer] = committer
-      options[:author] = committer
-      options[:commit] = {
-        message: message,
-        branch: ref,
-        update_ref: false,
+  def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
+    update_branch_with_hooks(user, branch) do |ref|
+      options = {
+        commit: {
+          branch: ref,
+          message: message,
+          update_ref: false
+        },
+        file: {
+          content: content,
+          path: path,
+          update: update
+        }
       }
 
-      options[:file] = {
-        content: content,
-        path: path,
-        update: update
-      }
+      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
 
       Gitlab::Git::Blob.commit(raw_repository, options)
     end
   end
 
-  def update_file(user, path, content, branch:, previous_path:, message:)
-    commit_with_hooks(user, branch) do |ref|
-      committer = user_to_committer(user)
-      options = {}
-      options[:committer] = committer
-      options[:author] = committer
-      options[:commit] = {
-        message: message,
-        branch: ref,
-        update_ref: false
+  def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
+    update_branch_with_hooks(user, branch) do |ref|
+      options = {
+        commit: {
+          branch: ref,
+          message: message,
+          update_ref: false
+        },
+        file: {
+          content: content,
+          path: path,
+          update: true
+        }
       }
 
-      options[:file] = {
-        content: content,
-        path: path,
-        update: true
-      }
+      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
 
-      if previous_path
+      if previous_path && previous_path != path
         options[:file][:previous_path] = previous_path
         Gitlab::Git::Blob.rename(raw_repository, options)
       else
@@ -770,34 +849,85 @@ class Repository
     end
   end
 
-  def remove_file(user, path, message, branch)
-    commit_with_hooks(user, branch) do |ref|
-      committer = user_to_committer(user)
-      options = {}
-      options[:committer] = committer
-      options[:author] = committer
-      options[:commit] = {
-        message: message,
-        branch: ref,
-        update_ref: false,
+  def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
+    update_branch_with_hooks(user, branch) do |ref|
+      options = {
+        commit: {
+          branch: ref,
+          message: message,
+          update_ref: false
+        },
+        file: {
+          path: path
+        }
       }
 
-      options[:file] = {
-        path: path
-      }
+      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
 
       Gitlab::Git::Blob.remove(raw_repository, options)
     end
   end
 
-  def user_to_committer(user)
+  def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
+    update_branch_with_hooks(user, branch) do |ref|
+      index = rugged.index
+      parents = []
+      branch = find_branch(ref)
+
+      if branch
+        last_commit = branch.dereferenced_target
+        index.read_tree(last_commit.raw_commit.tree)
+        parents = [last_commit.sha]
+      end
+
+      actions.each do |action|
+        case action[:action]
+        when :create, :update, :move
+          mode =
+            case action[:action]
+            when :update
+              index.get(action[:file_path])[:mode]
+            when :move
+              index.get(action[:previous_path])[:mode]
+            end
+          mode ||= 0o100644
+
+          index.remove(action[:previous_path]) if action[:action] == :move
+
+          content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
+          oid = rugged.write(content, :blob)
+
+          index.add(path: action[:file_path], oid: oid, mode: mode)
+        when :delete
+          index.remove(action[:file_path])
+        end
+      end
+
+      options = {
+        tree: index.write_tree(rugged),
+        message: message,
+        parents: parents
+      }
+      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
+      Rugged::Commit.create(rugged, options)
+    end
+  end
+
+  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
+
     {
-      email: user.email,
-      name: user.name,
-      time: Time.now
+      author: author,
+      committer: committer
     }
   end
 
+  def user_to_committer(user)
+    Gitlab::Git::committer_hash(email: user.email, name: user.name)
+  end
+
   def can_be_merged?(source_sha, target_branch)
     our_commit = rugged.branches[target_branch].target
     their_commit = rugged.lookup(source_sha)
@@ -819,7 +949,7 @@ class Repository
     merge_index = rugged.merge_commits(our_commit, their_commit)
     return false if merge_index.conflicts?
 
-    commit_with_hooks(user, merge_request.target_branch) do
+    update_branch_with_hooks(user, merge_request.target_branch) do
       actual_options = options.merge(
         parents: [our_commit, their_commit],
         tree: merge_index.write_tree(rugged),
@@ -832,12 +962,12 @@ class Repository
   end
 
   def revert(user, commit, base_branch, revert_tree_id = nil)
-    source_sha = find_branch(base_branch).target.sha
+    source_sha = find_branch(base_branch).dereferenced_target.sha
     revert_tree_id ||= check_revert_content(commit, base_branch)
 
     return false unless revert_tree_id
 
-    commit_with_hooks(user, base_branch) do
+    update_branch_with_hooks(user, base_branch) do
       committer = user_to_committer(user)
       source_sha = Rugged::Commit.create(rugged,
         message: commit.revert_message,
@@ -849,12 +979,12 @@ class Repository
   end
 
   def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
-    source_sha = find_branch(base_branch).target.sha
+    source_sha = find_branch(base_branch).dereferenced_target.sha
     cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
 
     return false unless cherry_pick_tree_id
 
-    commit_with_hooks(user, base_branch) do
+    update_branch_with_hooks(user, base_branch) do
       committer = user_to_committer(user)
       source_sha = Rugged::Commit.create(rugged,
         message: commit.message,
@@ -869,8 +999,16 @@ class Repository
     end
   end
 
+  def resolve_conflicts(user, branch, params)
+    update_branch_with_hooks(user, branch) do
+      committer = user_to_committer(user)
+
+      Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
+    end
+  end
+
   def check_revert_content(commit, base_branch)
-    source_sha = find_branch(base_branch).target.sha
+    source_sha = find_branch(base_branch).dereferenced_target.sha
     args       = [commit.id, source_sha]
     args << { mainline: 1 } if commit.merge_commit?
 
@@ -884,7 +1022,7 @@ class Repository
   end
 
   def check_cherry_pick_content(commit, base_branch)
-    source_sha = find_branch(base_branch).target.sha
+    source_sha = find_branch(base_branch).dereferenced_target.sha
     args       = [commit.id, source_sha]
     args << 1 if commit.merge_commit?
 
@@ -906,7 +1044,8 @@ class Repository
     root_ref_commit = commit(root_ref)
 
     if branch_commit
-      is_ancestor?(branch_commit.id, root_ref_commit.id)
+      same_head = branch_commit.id == root_ref_commit.id
+      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
     else
       nil
     end
@@ -925,59 +1064,31 @@ class Repository
   end
 
   def search_files(query, ref)
+    unless exists? && has_visible_content? && query.present?
+      return []
+    end
+
     offset = 2
     args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
     Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
   end
 
-  def parse_search_result(result)
-    ref = nil
-    filename = nil
-    basename = nil
-    startline = 0
-
-    result.each_line.each_with_index do |line, index|
-      if line =~ /^.*:.*:\d+:/
-        ref, filename, startline = line.split(':')
-        startline = startline.to_i - index
-        extname = Regexp.escape(File.extname(filename))
-        basename = filename.sub(/#{extname}$/, '')
-        break
-      end
-    end
-
-    data = ""
-
-    result.each_line do |line|
-      data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
-    end
-
-    OpenStruct.new(
-      filename: filename,
-      basename: basename,
-      ref: ref,
-      startline: startline,
-      data: data
-    )
-  end
-
   def fetch_ref(source_path, source_ref, target_ref)
     args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
     Gitlab::Popen.popen(args, path_to_repo)
   end
 
-  def commit_with_hooks(current_user, branch)
+  def create_ref(ref, ref_path)
+    fetch_ref(path_to_repo, ref, ref_path)
+  end
+
+  def update_branch_with_hooks(current_user, branch)
     update_autocrlf_option
 
-    oldrev = Gitlab::Git::BLANK_SHA
     ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
     target_branch = find_branch(branch)
     was_empty = empty?
 
-    if !was_empty && target_branch
-      oldrev = target_branch.target.id
-    end
-
     # Make commit
     newrev = yield(ref)
 
@@ -985,24 +1096,19 @@ class Repository
       raise CommitError.new('Failed to create commit')
     end
 
+    if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
+      oldrev = Gitlab::Git::BLANK_SHA
+    else
+      oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
+    end
+
     GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
-      if was_empty || !target_branch
-        # Create branch
-        rugged.references.create(ref, newrev)
+      update_ref!(ref, newrev, oldrev)
 
+      if was_empty || !target_branch
         # If repo was empty expire cache
         after_create if was_empty
         after_create_branch
-      else
-        # Update head
-        current_head = find_branch(branch).target.id
-
-        # Make sure target branch was not changed during pre-receive hook
-        if current_head == oldrev
-          rugged.references.update(ref, newrev)
-        else
-          raise CommitError.new('Commit was rejected because branch received new push')
-        end
       end
     end
 
@@ -1033,7 +1139,7 @@ class Repository
 
     @avatar ||= cache.fetch(:avatar) do
       AVATAR_FILES.find do |file|
-        blob_at_branch('master', file)
+        blob_at_branch(root_ref, file)
       end
     end
   end
@@ -1053,10 +1159,14 @@ class Repository
   end
 
   def tags_sorted_by_committed_date
-    tags.sort_by { |tag| tag.target.committed_date }
+    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
   end
 
   def keep_around_ref_name(sha)
     "refs/keep-around/#{sha}"
   end
+
+  def repository_event(event, tags = {})
+    Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
+  end
 end
diff --git a/app/models/service.rb b/app/models/service.rb
index 09b4717a523e63456bcfc26a5444d1e78e37020c..625fbc483029c000890adb1dec5ce9f6fa8ddb00 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,10 +7,12 @@ class Service < ActiveRecord::Base
   default_value_for :active, false
   default_value_for :push_events, true
   default_value_for :issues_events, true
+  default_value_for :confidential_issues_events, true
   default_value_for :merge_requests_events, true
   default_value_for :tag_push_events, true
   default_value_for :note_events, true
   default_value_for :build_events, true
+  default_value_for :pipeline_events, true
   default_value_for :wiki_page_events, true
 
   after_initialize :initialize_properties
@@ -33,6 +35,7 @@ class Service < ActiveRecord::Base
   scope :push_hooks, -> { where(push_events: true, active: true) }
   scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
   scope :issue_hooks, -> { where(issues_events: true, active: true) }
+  scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
   scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
   scope :note_hooks, -> { where(note_events: true, active: true) }
   scope :build_hooks, -> { where(build_events: true, active: true) }
@@ -100,7 +103,7 @@ class Service < ActiveRecord::Base
   end
 
   def supported_events
-    %w(push tag_push issue merge_request wiki_page)
+    %w(push tag_push issue confidential_issue merge_request wiki_page)
   end
 
   def execute(data)
@@ -133,6 +136,7 @@ class Service < ActiveRecord::Base
         end
 
         def #{arg}=(value)
+          self.properties ||= {}
           updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
           self.properties['#{arg}'] = value
         end
@@ -192,12 +196,13 @@ class Service < ActiveRecord::Base
   end
 
   def self.available_services_names
-    %w(
+    %w[
       asana
       assembla
       bamboo
       buildkite
       builds_email
+      pipelines_email
       bugzilla
       campfire
       custom_issue_tracker
@@ -214,7 +219,7 @@ class Service < ActiveRecord::Base
       redmine
       slack
       teamcity
-    )
+    ]
   end
 
   def self.create_from_template(project_id, template)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5ec933601ac8d2a8486e54ce90722587a748a549..2373b445009240494cc8e7c59e0b94545e71b70d 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,9 +1,20 @@
 class Snippet < ActiveRecord::Base
   include Gitlab::VisibilityLevel
   include Linguist::BlobHelper
+  include CacheMarkdownField
   include Participable
   include Referable
   include Sortable
+  include Awardable
+
+  cache_markdown_field :title, pipeline: :single_line
+  cache_markdown_field :content
+
+  # If file_name changes, it invalidates content
+  alias_method :default_content_html_invalidator, :content_html_invalidated?
+  def content_html_invalidated?
+    default_content_html_invalidator || file_name_changed?
+  end
 
   default_value_for :visibility_level, Snippet::PRIVATE
 
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8d7a5965aa15f4698fc869c3b39a3edadd39601c..f5ade1cc293cb0175ec157e6d29826d60d606261 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,4 +1,6 @@
 class Todo < ActiveRecord::Base
+  include Sortable
+
   ASSIGNED          = 1
   MENTIONED         = 2
   BUILD_FAILED      = 3
@@ -41,6 +43,29 @@ class Todo < ActiveRecord::Base
 
   after_save :keep_around_commit
 
+  class << self
+    def sort(method)
+      method == "priority" ? order_by_labels_priority : order_by(method)
+    end
+
+    # Order by priority depending on which issue/merge request the Todo belongs to
+    # Todos with highest priority first then oldest todos
+    # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
+    def order_by_labels_priority
+      params = {
+        target_type_column: "todos.target_type",
+        target_column: "todos.target_id",
+        project_column: "todos.project_id"
+      }
+
+      highest_priority = highest_label_priority(params).to_sql
+
+      select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+        order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
+        order('todos.created_at')
+    end
+  end
+
   def build_failed?
     action == BUILD_FAILED
   end
diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27e3732da1707b99c8c953ef98dfc8b930274201
--- /dev/null
+++ b/app/models/trending_project.rb
@@ -0,0 +1,35 @@
+class TrendingProject < ActiveRecord::Base
+  belongs_to :project
+
+  # The number of months to include in the trending calculation.
+  MONTHS_TO_INCLUDE = 1
+
+  # The maximum number of projects to include in the trending set.
+  PROJECTS_LIMIT = 100
+
+  # Populates the trending projects table with the current list of trending
+  # projects.
+  def self.refresh!
+    # The calculation **must** run in a transaction. If the removal of data and
+    # insertion of new data were to run separately a user might end up with an
+    # empty list of trending projects for a short period of time.
+    transaction do
+      delete_all
+
+      timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago)
+
+      connection.execute <<-EOF.strip_heredoc
+        INSERT INTO #{table_name} (project_id)
+        SELECT project_id
+        FROM notes
+        INNER JOIN projects ON projects.id = notes.project_id
+        WHERE notes.created_at >= #{timestamp}
+        AND notes.system IS FALSE
+        AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}
+        GROUP BY project_id
+        ORDER BY count(*) DESC
+        LIMIT #{PROJECTS_LIMIT};
+      EOF
+    end
+  end
+end
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 00b19686d482e985c1362f1c4ab9cf258fad754f..808acec098f9e23c40db5c757550d4c233f6f4ea 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -3,18 +3,19 @@
 class U2fRegistration < ActiveRecord::Base
   belongs_to :user
 
-  def self.register(user, app_id, json_response, challenges)
+  def self.register(user, app_id, params, challenges)
     u2f = U2F::U2F.new(app_id)
     registration = self.new
 
     begin
-      response = U2F::RegisterResponse.load_from_json(json_response)
+      response = U2F::RegisterResponse.load_from_json(params[:device_response])
       registration_data = u2f.register!(challenges, response)
       registration.update(certificate: registration_data.certificate,
                           key_handle: registration_data.key_handle,
                           public_key: registration_data.public_key,
                           counter: registration_data.counter,
-                          user: user)
+                          user: user,
+                          name: params[:name])
     rescue JSON::ParserError, NoMethodError, ArgumentError
       registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
     rescue U2F::Error => e
diff --git a/app/models/user.rb b/app/models/user.rb
index 48e83ab7e56278a22f3f20cd429d8e5dd884c112..3813df6684ea7955ac0cebc06cebb91b3d0c78f1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -13,6 +13,7 @@ class User < ActiveRecord::Base
   DEFAULT_NOTIFICATION_LEVEL = :participating
 
   add_authentication_token_field :authentication_token
+  add_authentication_token_field :incoming_email_token
 
   default_value_for :admin, false
   default_value_for(:external) { current_application_settings.user_default_external }
@@ -47,7 +48,7 @@ class User < ActiveRecord::Base
   #
 
   # Namespace for personal projects
-  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
+  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id
 
   # Profile
   has_many :keys, dependent: :destroy
@@ -66,17 +67,17 @@ class User < ActiveRecord::Base
   # Projects
   has_many :groups_projects,          through: :groups, source: :projects
   has_many :personal_projects,        through: :namespace, source: :projects
-  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember'
+  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy
   has_many :projects,                 through: :project_members
   has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
   has_many :users_star_projects, dependent: :destroy
   has_many :starred_projects, through: :users_star_projects, source: :project
 
-  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
+  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,   class_name: "Event"
+  has_many :events,                   dependent: :destroy, foreign_key: :author_id
   has_many :subscriptions,            dependent: :destroy
   has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
   has_many :assigned_issues,          dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
@@ -93,8 +94,10 @@ class User < ActiveRecord::Base
   #
   # Validations
   #
+  # Note: devise :validatable above adds validations for :email and :password
   validates :name, presence: true
-  validates :notification_email, presence: true, email: 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
   validates :bio, length: { maximum: 255 }, allow_blank: true
   validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -117,7 +120,7 @@ class User < ActiveRecord::Base
   before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
 
   after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
-  before_save :ensure_authentication_token
+  before_save :ensure_authentication_token, :ensure_incoming_email_token
   before_save :ensure_external_user_rights
   after_save :ensure_namespace_correct
   after_initialize :set_projects_limit
@@ -171,6 +174,7 @@ class User < ActiveRecord::Base
   scope :active, -> { with_state(:active) }
   scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
   scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
 
   def self.with_two_factor
     joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -256,6 +260,24 @@ class User < ActiveRecord::Base
       )
     end
 
+    # searches user by given pattern
+    # it compares name, email, username fields and user's secondary emails with given pattern
+    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+
+    def search_with_secondary_emails(query)
+      table = arel_table
+      email_table = Email.arel_table
+      pattern = "%#{query}%"
+      matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
+
+      where(
+        table[:name].matches(pattern).
+          or(table[:email].matches(pattern)).
+          or(table[:username].matches(pattern)).
+          or(table[:id].in(matched_by_emails_user_ids))
+      )
+    end
+
     def by_login(login)
       return nil unless login
 
@@ -279,6 +301,11 @@ class User < ActiveRecord::Base
       find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
     end
 
+    # Returns a user for the given SSH key.
+    def find_by_ssh_key_id(key_id)
+      find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
+    end
+
     def build_user(attrs = {})
       User.new(attrs)
     end
@@ -304,7 +331,7 @@ class User < ActiveRecord::Base
     username
   end
 
-  def to_reference(_from_project = nil)
+  def to_reference(_from_project = nil, _target_project = nil)
     "#{self.class.reference_prefix}#{username}"
   end
 
@@ -418,6 +445,16 @@ class User < ActiveRecord::Base
     Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
   end
 
+  # Returns the projects this user has reporter (or greater) access to, limited
+  # to at most the given projects.
+  #
+  # This method is useful when you have a list of projects and want to
+  # efficiently check to which of these projects the user has at least reporter
+  # access.
+  def projects_with_reporter_access_limited_to(projects)
+    authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
+  end
+
   def viewable_starred_projects
     starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
                            [Project::PUBLIC, Project::INTERNAL])
@@ -433,7 +470,7 @@ class User < ActiveRecord::Base
   #
   # This logic is duplicated from `Ability#project_abilities` into a SQL form.
   def projects_where_can_admin_issues
-    authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
+    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
   end
 
   def is_admin?
@@ -460,16 +497,12 @@ class User < ActiveRecord::Base
     can?(:create_group, nil)
   end
 
-  def abilities
-    Ability.abilities
-  end
-
   def can_select_namespace?
     several_namespaces? || admin
   end
 
   def can?(action, subject)
-    abilities.allowed?(self, action, subject)
+    Ability.allowed?(self, action, subject)
   end
 
   def first_name
@@ -489,10 +522,10 @@ class User < ActiveRecord::Base
     (personal_projects.count.to_f / projects_limit) * 100
   end
 
-  def recent_push(project_id = nil)
+  def recent_push(project_ids = nil)
     # Get push events not earlier than 2 hours ago
     events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
-    events = events.where(project_id: project_id) if project_id
+    events = events.where(project_id: project_ids) if project_ids
 
     # Use the latest event that has not been pushed or merged recently
     events.recent.find do |event|
@@ -588,6 +621,11 @@ class User < ActiveRecord::Base
   end
 
   def set_projects_limit
+    # `User.select(:id)` raises
+    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
+    # without this safeguard!
+    return unless self.has_attribute?(:projects_limit)
+
     connection_default_value_defined = new_record? && !projects_limit_changed?
     return unless self.projects_limit.nil? || connection_default_value_defined
 
@@ -831,6 +869,22 @@ class User < ActiveRecord::Base
     todos_pending_count(force: true)
   end
 
+  # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
+  # flow means we don't call that automatically (and can't conveniently do so).
+  #
+  # See:
+  #   <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
+  #
+  def increment_failed_attempts!
+    self.failed_attempts ||= 0
+    self.failed_attempts += 1
+    if attempts_exceeded?
+      lock_access! unless access_locked?
+    else
+      save(validate: false)
+    end
+  end
+
   private
 
   def projects_union(min_access_level = nil)
@@ -885,7 +939,7 @@ class User < ActiveRecord::Base
       if domain_matches?(allowed_domains, self.email)
         valid = true
       else
-        error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
+        error = "domain is not authorized for sign-up"
         valid = false
       end
     end
@@ -903,4 +957,13 @@ class User < ActiveRecord::Base
       signup_domain =~ regexp
     end
   end
+
+  def generate_token(token_field)
+    if token_field == :incoming_email_token
+      # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
+      SecureRandom.hex.to_i(16).to_s(36)
+    else
+      super
+    end
+  end
 end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..118c100ca11e3dd6c0f656c373a8b2d64e9a67cd
--- /dev/null
+++ b/app/policies/base_policy.rb
@@ -0,0 +1,116 @@
+class BasePolicy
+  class RuleSet
+    attr_reader :can_set, :cannot_set
+    def initialize(can_set, cannot_set)
+      @can_set = can_set
+      @cannot_set = cannot_set
+    end
+
+    def size
+      to_set.size
+    end
+
+    def self.empty
+      new(Set.new, Set.new)
+    end
+
+    def can?(ability)
+      @can_set.include?(ability) && !@cannot_set.include?(ability)
+    end
+
+    def include?(ability)
+      can?(ability)
+    end
+
+    def to_set
+      @can_set - @cannot_set
+    end
+
+    def merge(other)
+      @can_set.merge(other.can_set)
+      @cannot_set.merge(other.cannot_set)
+    end
+
+    def can!(*abilities)
+      @can_set.merge(abilities)
+    end
+
+    def cannot!(*abilities)
+      @cannot_set.merge(abilities)
+    end
+
+    def freeze
+      @can_set.freeze
+      @cannot_set.freeze
+      super
+    end
+  end
+
+  def self.abilities(user, subject)
+    new(user, subject).abilities
+  end
+
+  def self.class_for(subject)
+    return GlobalPolicy if subject.nil?
+
+    subject.class.ancestors.each do |klass|
+      next unless klass.name
+
+      begin
+        policy_class = "#{klass.name}Policy".constantize
+
+        # NOTE: the < operator here tests whether policy_class
+        # inherits from BasePolicy
+        return policy_class if policy_class < BasePolicy
+      rescue NameError
+        nil
+      end
+    end
+
+    raise "no policy for #{subject.class.name}"
+  end
+
+  attr_reader :user, :subject
+  def initialize(user, subject)
+    @user = user
+    @subject = subject
+  end
+
+  def abilities
+    return RuleSet.empty if @user && @user.blocked?
+    return anonymous_abilities if @user.nil?
+    collect_rules { rules }
+  end
+
+  def anonymous_abilities
+    collect_rules { anonymous_rules }
+  end
+
+  def anonymous_rules
+    rules
+  end
+
+  def delegate!(new_subject)
+    @rule_set.merge(Ability.allowed(@user, new_subject))
+  end
+
+  def can?(rule)
+    @rule_set.can?(rule)
+  end
+
+  def can!(*rules)
+    @rule_set.can!(*rules)
+  end
+
+  def cannot!(*rules)
+    @rule_set.cannot!(*rules)
+  end
+
+  private
+
+  def collect_rules(&b)
+    @rule_set = RuleSet.empty
+    yield
+    @rule_set
+  end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b25332b73ceeeb2635ae78d1d0665b36dca8017
--- /dev/null
+++ b/app/policies/ci/build_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+  class BuildPolicy < CommitStatusPolicy
+    def rules
+      super
+
+      # If we can't read build we should also not have that
+      # ability when looking at this in context of commit_status
+      %w[read create update admin].each do |rule|
+        cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
+      end
+    end
+  end
+end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d2eef1c50cf95b13a188870ec98a38355ceb1a3
--- /dev/null
+++ b/app/policies/ci/pipeline_policy.rb
@@ -0,0 +1,4 @@
+module Ci
+  class PipelinePolicy < BuildPolicy
+  end
+end
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7edd383530d794a5ccf66ebba4dd1f271a976c9b
--- /dev/null
+++ b/app/policies/ci/runner_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+  class RunnerPolicy < BasePolicy
+    def rules
+      return unless @user
+
+      can! :assign_runner if @user.is_admin?
+
+      return if @subject.is_shared? || @subject.locked?
+
+      can! :assign_runner if @user.ci_authorized_runners.include?(@subject)
+    end
+  end
+end
diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..593df738328045f390787de45c8ff54fc2b3d9a2
--- /dev/null
+++ b/app/policies/commit_status_policy.rb
@@ -0,0 +1,5 @@
+class CommitStatusPolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..163d070ff903447ac5929a95131d9d892bc8a3f3
--- /dev/null
+++ b/app/policies/deployment_policy.rb
@@ -0,0 +1,5 @@
+class DeploymentPolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4219569161e92d36d054a70237e8271c4cb35ba
--- /dev/null
+++ b/app/policies/environment_policy.rb
@@ -0,0 +1,5 @@
+class EnvironmentPolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9e28bd107a25699ac8c7eb69b23bda25bb9c0ff
--- /dev/null
+++ b/app/policies/external_issue_policy.rb
@@ -0,0 +1,5 @@
+class ExternalIssuePolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c2fbe6b56baa58386384ef32b957684a5a203ec
--- /dev/null
+++ b/app/policies/global_policy.rb
@@ -0,0 +1,8 @@
+class GlobalPolicy < BasePolicy
+  def rules
+    return unless @user
+
+    can! :create_group if @user.can_create_group
+    can! :read_users_list
+  end
+end
diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b34aa182eb4edc6cf461354b42ae189e588b514
--- /dev/null
+++ b/app/policies/group_label_policy.rb
@@ -0,0 +1,5 @@
+class GroupLabelPolicy < BasePolicy
+  def rules
+    delegate! @subject.group
+  end
+end
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62335527654cd230a2947a5f52ff97807f0f02fc
--- /dev/null
+++ b/app/policies/group_member_policy.rb
@@ -0,0 +1,19 @@
+class GroupMemberPolicy < BasePolicy
+  def rules
+    return unless @user
+
+    target_user = @subject.user
+    group = @subject.group
+
+    return if group.last_owner?(target_user)
+
+    can_manage = Ability.allowed?(@user, :admin_group_member, group)
+
+    if can_manage
+      can! :update_group_member
+      can! :destroy_group_member
+    elsif @user == target_user
+      can! :destroy_group_member
+    end
+  end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b65fb68cd88b93ce6e5e293fe91595f6c3d88ade
--- /dev/null
+++ b/app/policies/group_policy.rb
@@ -0,0 +1,46 @@
+class GroupPolicy < BasePolicy
+  def rules
+    can! :read_group if @subject.public?
+    return unless @user
+
+    globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
+    member = @subject.users.include?(@user)
+    owner = @user.admin? || @subject.has_owner?(@user)
+    master = owner || @subject.has_master?(@user)
+
+    can_read = false
+    can_read ||= globally_viewable
+    can_read ||= member
+    can_read ||= @user.admin?
+    can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
+    can! :read_group if can_read
+
+    # Only group masters and group owners can create new projects
+    if master
+      can! :create_projects
+      can! :admin_milestones
+      can! :admin_label
+    end
+
+    # Only group owner and administrators can admin group
+    if owner
+      can! :admin_group
+      can! :admin_namespace
+      can! :admin_group_member
+      can! :change_visibility_level
+    end
+
+    if globally_viewable && @subject.request_access_enabled && !member
+      can! :request_access
+    end
+  end
+
+  def can_read_group?
+    return true if @subject.public?
+    return true if @user.admin?
+    return true if @subject.internal? && !@user.external?
+    return true if @subject.users.include?(@user)
+
+    GroupProjectsFinder.new(@subject).execute(@user).any?
+  end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9501e499507b966aa7086e351647a77aa2e096c8
--- /dev/null
+++ b/app/policies/issuable_policy.rb
@@ -0,0 +1,14 @@
+class IssuablePolicy < BasePolicy
+  def action_name
+    @subject.class.name.underscore
+  end
+
+  def rules
+    if @user && @subject.assignee_or_author?(@user)
+      can! :"read_#{action_name}"
+      can! :"update_#{action_name}"
+    end
+
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..88f3179c6fff6c763a0bf6a62562f85d519292a0
--- /dev/null
+++ b/app/policies/issue_policy.rb
@@ -0,0 +1,27 @@
+class IssuePolicy < IssuablePolicy
+  # This class duplicates the same check of Issue#readable_by? for performance reasons
+  # Make sure to sync this class checks with issue.rb to avoid security problems.
+  # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
+
+  def issue
+    @subject
+  end
+
+  def rules
+    super
+
+    if @subject.confidential? && !can_read_confidential?
+      cannot! :read_issue
+      cannot! :update_issue
+      cannot! :admin_issue
+    end
+  end
+
+  private
+
+  def can_read_confidential?
+    return false unless @user
+
+    IssueCollection.new([@subject]).visible_to(@user).any?
+  end
+end
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bc3afc626fb7bebb3c4e241a3a20aee857437246
--- /dev/null
+++ b/app/policies/merge_request_policy.rb
@@ -0,0 +1,3 @@
+class MergeRequestPolicy < IssuablePolicy
+  # pass
+end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29bb357e00a8c63b53267d160d91974ebb62bb1b
--- /dev/null
+++ b/app/policies/namespace_policy.rb
@@ -0,0 +1,10 @@
+class NamespacePolicy < BasePolicy
+  def rules
+    return unless @user
+
+    if @subject.owner == @user || @user.admin?
+      can! :create_projects
+      can! :admin_namespace
+    end
+  end
+end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83847466ee23ef3e35ce1b79a31f400961c8efd0
--- /dev/null
+++ b/app/policies/note_policy.rb
@@ -0,0 +1,19 @@
+class NotePolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+
+    return unless @user
+
+    if @subject.author == @user
+      can! :read_note
+      can! :update_note
+      can! :admin_note
+      can! :resolve_note
+    end
+
+    if @subject.for_merge_request? &&
+       @subject.noteable.author == @user
+      can! :resolve_note
+    end
+  end
+end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..46c5aa1a5be4849b8498d856873707af84b6b8b7
--- /dev/null
+++ b/app/policies/personal_snippet_policy.rb
@@ -0,0 +1,16 @@
+class PersonalSnippetPolicy < BasePolicy
+  def rules
+    can! :read_personal_snippet if @subject.public?
+    return unless @user
+
+    if @subject.author == @user
+      can! :read_personal_snippet
+      can! :update_personal_snippet
+      can! :admin_personal_snippet
+    end
+
+    if @subject.internal? && !@user.external?
+      can! :read_personal_snippet
+    end
+  end
+end
diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b12b4c5166b3dd7066718a6de24e24202f4ea940
--- /dev/null
+++ b/app/policies/project_label_policy.rb
@@ -0,0 +1,5 @@
+class ProjectLabelPolicy < BasePolicy
+  def rules
+    delegate! @subject.project
+  end
+end
diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1c038dddd4bdd0b991aed4f55921747e24a9c741
--- /dev/null
+++ b/app/policies/project_member_policy.rb
@@ -0,0 +1,22 @@
+class ProjectMemberPolicy < BasePolicy
+  def rules
+    # anonymous users have no abilities here
+    return unless @user
+
+    target_user = @subject.user
+    project = @subject.project
+
+    return if target_user == project.owner
+
+    can_manage = Ability.allowed?(@user, :admin_project_member, project)
+
+    if can_manage
+      can! :update_project_member
+      can! :destroy_project_member
+    end
+
+    if @user == target_user
+      can! :destroy_project_member
+    end
+  end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1ee31023e26628b6f184d1b76c36dbe8372ef3f2
--- /dev/null
+++ b/app/policies/project_policy.rb
@@ -0,0 +1,256 @@
+class ProjectPolicy < BasePolicy
+  def rules
+    team_access!(user)
+
+    owner = project.owner == user ||
+            (project.group && project.group.has_owner?(user))
+
+    owner_access! if user.admin? || owner
+    team_member_owner_access! if owner
+
+    if project.public? || (project.internal? && !user.external?)
+      guest_access!
+      public_access!
+
+      # Allow to read builds for internal projects
+      can! :read_build if project.public_builds?
+
+      if project.request_access_enabled &&
+         !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
+        can! :request_access
+      end
+    end
+
+    archived_access! if project.archived?
+
+    disabled_features!
+  end
+
+  def project
+    @subject
+  end
+
+  def guest_access!
+    can! :read_project
+    can! :read_board
+    can! :read_list
+    can! :read_wiki
+    can! :read_issue
+    can! :read_label
+    can! :read_milestone
+    can! :read_project_snippet
+    can! :read_project_member
+    can! :read_note
+    can! :create_project
+    can! :create_issue
+    can! :create_note
+    can! :upload_file
+    can! :read_cycle_analytics
+  end
+
+  def reporter_access!
+    can! :download_code
+    can! :fork_project
+    can! :create_project_snippet
+    can! :update_issue
+    can! :admin_issue
+    can! :admin_label
+    can! :admin_list
+    can! :read_commit_status
+    can! :read_build
+    can! :read_container_image
+    can! :read_pipeline
+    can! :read_environment
+    can! :read_deployment
+    can! :read_merge_request
+  end
+
+  # Permissions given when an user is team member of a project
+  def team_member_reporter_access!
+    can! :build_download_code
+    can! :build_read_container_image
+  end
+
+  def developer_access!
+    can! :admin_merge_request
+    can! :update_merge_request
+    can! :create_commit_status
+    can! :update_commit_status
+    can! :create_build
+    can! :update_build
+    can! :create_pipeline
+    can! :update_pipeline
+    can! :create_merge_request
+    can! :create_wiki
+    can! :push_code
+    can! :resolve_note
+    can! :create_container_image
+    can! :update_container_image
+    can! :create_environment
+    can! :create_deployment
+  end
+
+  def master_access!
+    can! :push_code_to_protected_branches
+    can! :update_project_snippet
+    can! :update_environment
+    can! :update_deployment
+    can! :admin_milestone
+    can! :admin_project_snippet
+    can! :admin_project_member
+    can! :admin_note
+    can! :admin_wiki
+    can! :admin_project
+    can! :admin_commit_status
+    can! :admin_build
+    can! :admin_container_image
+    can! :admin_pipeline
+    can! :admin_environment
+    can! :admin_deployment
+  end
+
+  def public_access!
+    can! :download_code
+    can! :fork_project
+    can! :read_commit_status
+    can! :read_pipeline
+    can! :read_container_image
+    can! :build_download_code
+    can! :build_read_container_image
+    can! :read_merge_request
+  end
+
+  def owner_access!
+    guest_access!
+    reporter_access!
+    developer_access!
+    master_access!
+    can! :change_namespace
+    can! :change_visibility_level
+    can! :rename_project
+    can! :remove_project
+    can! :archive_project
+    can! :remove_fork_project
+    can! :destroy_merge_request
+    can! :destroy_issue
+  end
+
+  def team_member_owner_access!
+    team_member_reporter_access!
+  end
+
+  # Push abilities on the users team role
+  def team_access!(user)
+    access = project.team.max_member_access(user.id)
+
+    return if access < Gitlab::Access::GUEST
+    guest_access!
+
+    return if access < Gitlab::Access::REPORTER
+    reporter_access!
+    team_member_reporter_access!
+
+    return if access < Gitlab::Access::DEVELOPER
+    developer_access!
+
+    return if access < Gitlab::Access::MASTER
+    master_access!
+  end
+
+  def archived_access!
+    cannot! :create_merge_request
+    cannot! :push_code
+    cannot! :push_code_to_protected_branches
+    cannot! :update_merge_request
+    cannot! :admin_merge_request
+  end
+
+  def disabled_features!
+    repository_enabled = project.feature_available?(:repository, user)
+
+    unless project.feature_available?(:issues, user)
+      cannot!(*named_abilities(:issue))
+    end
+
+    unless project.feature_available?(:merge_requests, user) && repository_enabled
+      cannot!(*named_abilities(:merge_request))
+    end
+
+    unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user)
+      cannot!(*named_abilities(:label))
+      cannot!(*named_abilities(:milestone))
+    end
+
+    unless project.feature_available?(:snippets, user)
+      cannot!(*named_abilities(:project_snippet))
+    end
+
+    unless project.feature_available?(:wiki, user) || project.has_external_wiki?
+      cannot!(*named_abilities(:wiki))
+    end
+
+    unless project.feature_available?(:builds, user) && repository_enabled
+      cannot!(*named_abilities(:build))
+      cannot!(*named_abilities(:pipeline))
+      cannot!(*named_abilities(:environment))
+      cannot!(*named_abilities(:deployment))
+    end
+
+    unless repository_enabled
+      cannot! :push_code
+      cannot! :push_code_to_protected_branches
+      cannot! :download_code
+      cannot! :fork_project
+      cannot! :read_commit_status
+    end
+
+    unless project.container_registry_enabled
+      cannot!(*named_abilities(:container_image))
+    end
+  end
+
+  def anonymous_rules
+    return unless project.public?
+
+    can! :read_project
+    can! :read_board
+    can! :read_list
+    can! :read_wiki
+    can! :read_label
+    can! :read_milestone
+    can! :read_project_snippet
+    can! :read_project_member
+    can! :read_merge_request
+    can! :read_note
+    can! :read_pipeline
+    can! :read_commit_status
+    can! :read_container_image
+    can! :download_code
+    can! :read_cycle_analytics
+
+    # NOTE: may be overridden by IssuePolicy
+    can! :read_issue
+
+    # Allow to read builds by anonymous user if guests are allowed
+    can! :read_build if project.public_builds?
+
+    disabled_features!
+  end
+
+  def project_group_member?(user)
+    project.group &&
+    (
+      project.group.members.exists?(user_id: user.id) ||
+      project.group.requesters.exists?(user_id: user.id)
+    )
+  end
+
+  def named_abilities(name)
+    [
+      :"read_#{name}",
+      :"create_#{name}",
+      :"update_#{name}",
+      :"admin_#{name}"
+    ]
+  end
+end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..57acccfafd95c67b661fa923bb676e34d8124361
--- /dev/null
+++ b/app/policies/project_snippet_policy.rb
@@ -0,0 +1,20 @@
+class ProjectSnippetPolicy < BasePolicy
+  def rules
+    can! :read_project_snippet if @subject.public?
+    return unless @user
+
+    if @user && @subject.author == @user || @user.admin?
+      can! :read_project_snippet
+      can! :update_project_snippet
+      can! :admin_project_snippet
+    end
+
+    if @subject.internal? && !@user.external?
+      can! :read_project_snippet
+    end
+
+    if @subject.private? && @subject.project.team.member?(@user)
+      can! :read_project_snippet
+    end
+  end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03a2499e2638b2e46401b1f3c8a152c67ebac6ea
--- /dev/null
+++ b/app/policies/user_policy.rb
@@ -0,0 +1,11 @@
+class UserPolicy < BasePolicy
+  include Gitlab::CurrentSettings
+
+  def rules
+    can! :read_user if @user || !restricted_public_level?
+  end
+
+  def restricted_public_level?
+    current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
+  end
+end
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..de9a181db90f0303e3f70385aa5332dc2b1fae8b
--- /dev/null
+++ b/app/serializers/base_serializer.rb
@@ -0,0 +1,18 @@
+class BaseSerializer
+  def initialize(parameters = {})
+    @request = EntityRequest.new(parameters)
+  end
+
+  def represent(resource, opts = {})
+    self.class.entity_class
+      .represent(resource, opts.merge(request: @request))
+  end
+
+  def self.entity(entity_class)
+    @entity_class ||= entity_class
+  end
+
+  def self.entity_class
+    @entity_class
+  end
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d9ac66de0e5a1d5d96dace4acebea48030e27c9
--- /dev/null
+++ b/app/serializers/build_entity.rb
@@ -0,0 +1,24 @@
+class BuildEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :id
+  expose :name
+
+  expose :build_url do |build|
+    url_to(:namespace_project_build, build)
+  end
+
+  expose :retry_url do |build|
+    url_to(:retry_namespace_project_build, build)
+  end
+
+  expose :play_url, if: ->(build, _) { build.manual? } do |build|
+    url_to(:play_namespace_project_build, build)
+  end
+
+  private
+
+  def url_to(route, build)
+    send("#{route}_url", build.project.namespace, build.project, build)
+  end
+end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f7eba6fc1e34d8f54a6d1f672340b688c4e66af6
--- /dev/null
+++ b/app/serializers/commit_entity.rb
@@ -0,0 +1,12 @@
+class CommitEntity < API::Entities::RepoCommit
+  include RequestAwareEntity
+
+  expose :author, using: UserEntity
+
+  expose :commit_url do |commit|
+    namespace_project_tree_url(
+      request.project.namespace,
+      request.project,
+      id: commit.id)
+  end
+end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ad6fc8d665bd571aa8289e431eb30482ff46ed30
--- /dev/null
+++ b/app/serializers/deployment_entity.rb
@@ -0,0 +1,27 @@
+class DeploymentEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :id
+  expose :iid
+  expose :sha
+
+  expose :ref do
+    expose :name do |deployment|
+      deployment.ref
+    end
+
+    expose :ref_url do |deployment|
+      namespace_project_tree_url(
+        deployment.project.namespace,
+        deployment.project,
+        id: deployment.ref)
+    end
+  end
+
+  expose :tag
+  expose :last?
+  expose :user, using: UserEntity
+  expose :commit, using: CommitEntity
+  expose :deployable, using: BuildEntity
+  expose :manual_actions, using: BuildEntity
+end
diff --git a/app/serializers/entity_request.rb b/app/serializers/entity_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..456ba1174c0634486d37a9bb036a252b17c2e674
--- /dev/null
+++ b/app/serializers/entity_request.rb
@@ -0,0 +1,12 @@
+class EntityRequest
+  # We use EntityRequest object to collect parameters and variables
+  # from the controller. Because options that are being passed to the entity
+  # do appear in each entity object  in the chain, we need a way to pass data
+  # that is present in the controller (see  #20045).
+  #
+  def initialize(parameters)
+    parameters.each do |key, value|
+      define_singleton_method(key) { value }
+    end
+  end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee4392cc46d462de2da0cd8c7d64f553e8b8b4b6
--- /dev/null
+++ b/app/serializers/environment_entity.rb
@@ -0,0 +1,20 @@
+class EnvironmentEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :id
+  expose :name
+  expose :state
+  expose :external_url
+  expose :environment_type
+  expose :last_deployment, using: DeploymentEntity
+  expose :stoppable?
+
+  expose :environment_url do |environment|
+    namespace_project_environment_url(
+      environment.project.namespace,
+      environment.project,
+      environment)
+  end
+
+  expose :created_at, :updated_at
+end
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..91955542f2540bda42e7c0fb58c515a2902ffde5
--- /dev/null
+++ b/app/serializers/environment_serializer.rb
@@ -0,0 +1,3 @@
+class EnvironmentSerializer < BaseSerializer
+  entity EnvironmentEntity
+end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ff8c1142abc114809dc57354197d7b46d602a35c
--- /dev/null
+++ b/app/serializers/request_aware_entity.rb
@@ -0,0 +1,11 @@
+module RequestAwareEntity
+  extend ActiveSupport::Concern
+
+  included do
+    include Gitlab::Routing.url_helpers
+  end
+
+  def request
+    @options.fetch(:request)
+  end
+end
diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..43754ea94f734143b27c58368c95b6a6a11ecc1a
--- /dev/null
+++ b/app/serializers/user_entity.rb
@@ -0,0 +1,2 @@
+class UserEntity < API::Entities::UserBasic
+end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index 5c60addbe7c4d120e15c62d8f33654d38ec38081..76b9f1feda776b76b2fa55691ffae3a756100996 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -29,25 +29,25 @@ class AkismetService
   end
 
   def submit_ham
-    return false unless akismet_enabled?
+    submit(:ham)
+  end
 
-    params = {
-      type: 'comment',
-      text: text,
-      author: owner.name,
-      author_email: owner.email
-    }
+  def submit_spam
+    submit(:spam)
+  end
 
-    begin
-      akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
-      true
-    rescue => e
-      Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
-      false
-    end
+  private
+
+  def akismet_client
+    @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+                                              Gitlab.config.gitlab.url)
   end
 
-  def submit_spam
+  def akismet_enabled?
+    current_application_settings.akismet_enabled
+  end
+
+  def submit(type)
     return false unless akismet_enabled?
 
     params = {
@@ -58,22 +58,11 @@ class AkismetService
     }
 
     begin
-      akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+      akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
       true
     rescue => e
       Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
       false
     end
   end
-
-  private
-
-  def akismet_client
-    @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
-                                              Gitlab.config.gitlab.url)
-  end
-
-  def akismet_enabled?
-    current_application_settings.akismet_enabled
-  end
 end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 6072123b851e3ffb1d9c6dab9322e9f407aafd4e..c00c5aebf57e776dc92e6439fc341e991dea6782 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -4,11 +4,13 @@ module Auth
 
     AUDIENCE = 'container_registry'
 
-    def execute
-      return error('not found', 404) unless registry.enabled
+    def execute(authentication_abilities:)
+      @authentication_abilities = authentication_abilities
 
-      unless current_user || project
-        return error('forbidden', 403) unless scope
+      return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
+
+      unless scope || current_user || project
+        return error('DENIED', status: 403, message: 'access forbidden')
       end
 
       { token: authorized_token(scope).encoded }
@@ -74,9 +76,9 @@ module Auth
 
       case requested_action
       when 'pull'
-        requested_project == project || can?(current_user, :read_container_image, requested_project)
+        build_can_pull?(requested_project) || user_can_pull?(requested_project)
       when 'push'
-        requested_project == project || can?(current_user, :create_container_image, requested_project)
+        build_can_push?(requested_project) || user_can_push?(requested_project)
       else
         false
       end
@@ -85,5 +87,40 @@ module Auth
     def registry
       Gitlab.config.registry
     end
+
+    def build_can_pull?(requested_project)
+      # Build can:
+      # 1. pull from its own project (for ex. a build)
+      # 2. read images from dependent projects if creator of build is a team member
+      has_authentication_ability?(:build_read_container_image) &&
+        (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
+    end
+
+    def user_can_pull?(requested_project)
+      has_authentication_ability?(:read_container_image) &&
+        can?(current_user, :read_container_image, requested_project)
+    end
+
+    def build_can_push?(requested_project)
+      # Build can push only to the project from which it originates
+      has_authentication_ability?(:build_create_container_image) &&
+        requested_project == project
+    end
+
+    def user_can_push?(requested_project)
+      has_authentication_ability?(:create_container_image) &&
+        can?(current_user, :create_container_image, requested_project)
+    end
+
+    def error(code, status:, message: '')
+      {
+        errors: [{ code: code, message: message }],
+        http_status: status
+      }
+    end
+
+    def has_authentication_ability?(capability)
+      (@authentication_abilities || []).include?(capability)
+    end
   end
 end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 0d55ba5a9816a944123058c034de268b9bd88f7b..1a2bad77a0291e6edfef7be253bdd5fdbed91262 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -7,12 +7,8 @@ class BaseService
     @project, @current_user, @params = project, user, params.dup
   end
 
-  def abilities
-    Ability.abilities
-  end
-
   def can?(object, action, subject)
-    abilities.allowed?(object, action, subject)
+    Ability.allowed?(object, action, subject)
   end
 
   def notification_service
@@ -60,9 +56,8 @@ class BaseService
     result
   end
 
-  def success
-    {
-      status: :success
-    }
+  def success(pass_back = {})
+    pass_back[:status] = :success
+    pass_back
   end
 end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9bdd7b6f0cf16034c904b1faae7a302d4e050cfa
--- /dev/null
+++ b/app/services/boards/create_service.rb
@@ -0,0 +1,21 @@
+module Boards
+  class CreateService < BaseService
+    def execute
+      if project.boards.empty?
+        create_board!
+      else
+        project.boards.first
+      end
+    end
+
+    private
+
+    def create_board!
+      board = project.boards.create
+      board.lists.create(list_type: :backlog)
+      board.lists.create(list_type: :done)
+
+      board
+    end
+  end
+end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c0d7ff5b585a5bb3c3c07d96d0f4e43eda1d1f8d
--- /dev/null
+++ b/app/services/boards/issues/create_service.rb
@@ -0,0 +1,23 @@
+module Boards
+  module Issues
+    class CreateService < BaseService
+      def execute
+        create_issue(params.merge(label_ids: [list.label_id]))
+      end
+
+      private
+
+      def board
+        @board ||= project.boards.find(params.delete(:board_id))
+      end
+
+      def list
+        @list ||= board.lists.find(params.delete(:list_id))
+      end
+
+      def create_issue(params)
+        ::Issues::CreateService.new(project, current_user, params).execute
+      end
+    end
+  end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd4a462c7b2faa831cd77510360275c75ca38100
--- /dev/null
+++ b/app/services/boards/issues/list_service.rb
@@ -0,0 +1,67 @@
+module Boards
+  module Issues
+    class ListService < BaseService
+      def execute
+        issues = IssuesFinder.new(current_user, filter_params).execute
+        issues = without_board_labels(issues) unless list.movable?
+        issues = with_list_label(issues) if list.movable?
+        issues
+      end
+
+      private
+
+      def board
+        @board ||= project.boards.find(params[:board_id])
+      end
+
+      def list
+        @list ||= board.lists.find(params[:id])
+      end
+
+      def filter_params
+        set_default_scope
+        set_default_sort
+        set_project
+        set_state
+
+        params
+      end
+
+      def set_default_scope
+        params[:scope] = 'all'
+      end
+
+      def set_default_sort
+        params[:sort] = 'priority'
+      end
+
+      def set_project
+        params[:project_id] = project.id
+      end
+
+      def set_state
+        params[:state] = list.done? ? 'closed' : 'opened'
+      end
+
+      def board_label_ids
+        @board_label_ids ||= board.lists.movable.pluck(:label_id)
+      end
+
+      def without_board_labels(issues)
+        return issues unless board_label_ids.any?
+
+        issues.where.not(
+          LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+                   .where(label_id: board_label_ids).limit(1).arel.exists
+        )
+      end
+
+      def with_list_label(issues)
+        issues.where(
+          LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+                   .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists
+        )
+      end
+    end
+  end
+end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96554a92a027ae83c5fc6519085d9c6b190cc536
--- /dev/null
+++ b/app/services/boards/issues/move_service.rb
@@ -0,0 +1,63 @@
+module Boards
+  module Issues
+    class MoveService < BaseService
+      def execute(issue)
+        return false unless can?(current_user, :update_issue, issue)
+        return false unless valid_move?
+
+        update_service.execute(issue)
+      end
+
+      private
+
+      def board
+        @board ||= project.boards.find(params[:board_id])
+      end
+
+      def valid_move?
+        moving_from_list.present? && moving_to_list.present? &&
+          moving_from_list != moving_to_list
+      end
+
+      def moving_from_list
+        @moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
+      end
+
+      def moving_to_list
+        @moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
+      end
+
+      def update_service
+        ::Issues::UpdateService.new(project, current_user, issue_params)
+      end
+
+      def issue_params
+        {
+          add_label_ids: add_label_ids,
+          remove_label_ids: remove_label_ids,
+          state_event: issue_state
+        }
+      end
+
+      def issue_state
+        return 'reopen' if moving_from_list.done?
+        return 'close'  if moving_to_list.done?
+      end
+
+      def add_label_ids
+        [moving_to_list.label_id].compact
+      end
+
+      def remove_label_ids
+        label_ids =
+          if moving_to_list.movable?
+            moving_from_list.label_id
+          else
+            project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
+          end
+
+        Array(label_ids).compact
+      end
+    end
+  end
+end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84f1fc3a4e297bc55856ac5d8148df1ce44a8a7f
--- /dev/null
+++ b/app/services/boards/list_service.rb
@@ -0,0 +1,14 @@
+module Boards
+  class ListService < BaseService
+    def execute
+      create_board! if project.boards.empty?
+      project.boards
+    end
+
+    private
+
+    def create_board!
+      Boards::CreateService.new(project, current_user).execute
+    end
+  end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe0d762ccd26d80ce58c56c2db502c6ed2e7f7ea
--- /dev/null
+++ b/app/services/boards/lists/create_service.rb
@@ -0,0 +1,29 @@
+module Boards
+  module Lists
+    class CreateService < BaseService
+      def execute(board)
+        List.transaction do
+          label    = available_labels.find(params[:label_id])
+          position = next_position(board)
+
+          create_list(board, label, position)
+        end
+      end
+
+      private
+
+      def available_labels
+        LabelsFinder.new(current_user, project_id: project.id).execute
+      end
+
+      def next_position(board)
+        max_position = board.lists.movable.maximum(:position)
+        max_position.nil? ? 0 : max_position.succ
+      end
+
+      def create_list(board, label, position)
+        board.lists.create(label: label, list_type: :label, position: position)
+      end
+    end
+  end
+end
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f986e05944c336281cd301f1938062fbee3c9a79
--- /dev/null
+++ b/app/services/boards/lists/destroy_service.rb
@@ -0,0 +1,29 @@
+module Boards
+  module Lists
+    class DestroyService < BaseService
+      def execute(list)
+        return false unless list.destroyable?
+
+        @board = list.board
+
+        list.with_lock do
+          decrement_higher_lists(list)
+          remove_list(list)
+        end
+      end
+
+      private
+
+      attr_reader :board
+
+      def decrement_higher_lists(list)
+        board.lists.movable.where('position > ?',  list.position)
+                   .update_all('position = position - 1')
+      end
+
+      def remove_list(list)
+        list.destroy
+      end
+    end
+  end
+end
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..939f9bfd068cd5a56ed4141f4a53c482bd9c464e
--- /dev/null
+++ b/app/services/boards/lists/generate_service.rb
@@ -0,0 +1,33 @@
+module Boards
+  module Lists
+    class GenerateService < BaseService
+      def execute(board)
+        return false unless board.lists.movable.empty?
+
+        List.transaction do
+          label_params.each { |params| create_list(board, params) }
+        end
+
+        true
+      end
+
+      private
+
+      def create_list(board, params)
+        label = find_or_create_label(params)
+        Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
+      end
+
+      def find_or_create_label(params)
+        ::Labels::FindOrCreateService.new(current_user, project, params).execute
+      end
+
+      def label_params
+        [
+          { name: 'To Do', color: '#F0AD4E' },
+          { name: 'Doing', color: '#5CB85C' }
+        ]
+      end
+    end
+  end
+end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c579ed4c869eb118f489d590f89800d635372144
--- /dev/null
+++ b/app/services/boards/lists/list_service.rb
@@ -0,0 +1,9 @@
+module Boards
+  module Lists
+    class ListService < BaseService
+      def execute(board)
+        board.lists
+      end
+    end
+  end
+end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2a68865f7b82fe5b88d085a554b33b53abc6177
--- /dev/null
+++ b/app/services/boards/lists/move_service.rb
@@ -0,0 +1,52 @@
+module Boards
+  module Lists
+    class MoveService < BaseService
+      def execute(list)
+        @board = list.board
+        @old_position = list.position
+        @new_position = params[:position]
+
+        return false unless list.movable?
+        return false unless valid_move?
+
+        list.with_lock do
+          reorder_intermediate_lists
+          update_list_position(list)
+        end
+      end
+
+      private
+
+      attr_reader :board, :old_position, :new_position
+
+      def valid_move?
+        new_position.present? && new_position != old_position &&
+          new_position >= 0 && new_position < board.lists.movable.size
+      end
+
+      def reorder_intermediate_lists
+        if old_position < new_position
+          decrement_intermediate_lists
+        else
+          increment_intermediate_lists
+        end
+      end
+
+      def decrement_intermediate_lists
+        board.lists.movable.where('position > ?',  old_position)
+                           .where('position <= ?', new_position)
+                           .update_all('position = position - 1')
+      end
+
+      def increment_intermediate_lists
+        board.lists.movable.where('position >= ?', new_position)
+                           .where('position < ?',  old_position)
+                           .update_all('position = position + 1')
+      end
+
+      def update_list_position(list)
+        list.update_attribute(:position, new_position)
+      end
+    end
+  end
+end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 6f7610d42ba0ced02417ff428267fb0a6e9d34fc..8face432d97fc52dc0fbd6b7ef083c6a6fb6c1e2 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -15,7 +15,8 @@ module Ci
           process_stage(index)
         end
 
-      # Return a flag if a when builds got enqueued
+      @pipeline.update_status
+
       new_builds.flatten.any?
     end
 
@@ -28,14 +29,16 @@ module Ci
     def process_stage(index)
       current_status = status_for_prior_stages(index)
 
-      created_builds_in_stage(index).select do |build|
-        process_build(build, current_status)
+      if HasStatus::COMPLETED_STATUSES.include?(current_status)
+        created_builds_in_stage(index).select do |build|
+          Gitlab::OptimisticLocking.retry_lock(build) do |subject|
+            process_build(subject, current_status)
+          end
+        end
       end
     end
 
     def process_build(build, current_status)
-      return false unless Statuseable::COMPLETED_STATUSES.include?(current_status)
-
       if valid_statuses_for_when(build.when).include?(current_status)
         build.enqueue
         true
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 9a187f5d6945e63c76d0beeb73f75829278ca9a4..74b5ebf372b10523f873c7d4d2d232f1ff0d2e62 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -8,16 +8,18 @@ module Ci
       builds =
         if current_runner.shared?
           builds.
-            # don't run projects which have not enabled shared runners
-            joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
+            # 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').
 
             # 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").
+            where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
             order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
         else
           # do run projects which are only assigned to this runner (FIFO)
-          builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
+          builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
         end
 
       build = builds.find do |build|
@@ -26,17 +28,14 @@ module Ci
 
       if build
         # In case when 2 runners try to assign the same build, second runner will be declined
-        # with StateMachines::InvalidTransition in run! method.
-        build.with_lock do
-          build.runner_id = current_runner.id
-          build.save!
-          build.run!
-        end
+        # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
+        build.runner_id = current_runner.id
+        build.run!
       end
 
       build
 
-    rescue StateMachines::InvalidTransition
+    rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
       nil
     end
 
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
deleted file mode 100644
index 92e6df442b4f3f9ee929077a254c81684ed3571c..0000000000000000000000000000000000000000
--- a/app/services/ci/web_hook_service.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Ci
-  class WebHookService
-    def build_end(build)
-      execute_hooks(build.project, build_data(build))
-    end
-
-    def execute_hooks(project, data)
-      project.web_hooks.each do |web_hook|
-        async_execute_hook(web_hook, data)
-      end
-    end
-
-    def async_execute_hook(hook, data)
-      Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
-    end
-
-    def build_data(build)
-      project = build.project
-      data = {}
-      data.merge!({
-        build_id: build.id,
-        build_name: build.name,
-        build_status: build.status,
-        build_started_at: build.started_at,
-        build_finished_at: build.finished_at,
-        project_id: project.id,
-        project_name: project.name,
-        gitlab_url: project.gitlab_url,
-        ref: build.ref,
-        before_sha: build.before_sha,
-        sha: build.sha,
-      })
-    end
-  end
-end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index ed73d8cb8c2a13f911d4d8a86131a9ed62f8b10d..1c82599c5793d3f92db2ba4d4ee502a0e3f3ab58 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -16,11 +16,29 @@ module Commits
       error(ex.message)
     end
 
+    private
+
     def commit
       raise NotImplementedError
     end
 
-    private
+    def commit_change(action)
+      raise NotImplementedError unless repository.respond_to?(action)
+
+      into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
+      tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+
+      if tree_id
+        create_target_branch(into) if @create_merge_request
+
+        repository.public_send(action, current_user, @commit, into, tree_id)
+        success
+      else
+        error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically.
+                     It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+        raise ChangeError, error_msg
+      end
+    end
 
     def check_push_permissions
       allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
index f9a4efa7182ead1398cfbd02fed217e9acc510c6..605cca36f9c39d9d99355d0ba02b67a40a859785 100644
--- a/app/services/commits/cherry_pick_service.rb
+++ b/app/services/commits/cherry_pick_service.rb
@@ -1,19 +1,7 @@
 module Commits
   class CherryPickService < ChangeService
     def commit
-      cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
-      cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
-
-      if cherry_pick_tree_id
-        create_target_branch(cherry_pick_into) if @create_merge_request
-
-        repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
-        success
-      else
-        error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
-                     It may have already been cherry-picked, or a more recent commit may have updated some of its content."
-        raise ChangeError, error_msg
-      end
+      commit_change(:cherry_pick)
     end
   end
 end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index c7de9f6f35e47cf0c704a8136abcfb4996666e19..addd55cb32f1c99136cd80056605a85844a77879 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,19 +1,7 @@
 module Commits
   class RevertService < ChangeService
     def commit
-      revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
-      revert_tree_id = repository.check_revert_content(@commit, @target_branch)
-
-      if revert_tree_id
-        create_target_branch(revert_into) if @create_merge_request
-
-        repository.revert(current_user, @commit, revert_into, revert_tree_id)
-        success
-      else
-        error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
-                     It may have already been reverted, or a more recent commit may have updated some of its content."
-        raise ChangeError, error_msg
-      end
+      commit_change(:revert)
     end
   end
 end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 6d6075628af89a0c113a2f637af5b728f3009d60..5e8fafca98c7b71be4de700a42bff260960f5537 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
 # Compare 2 branches for one repo or between repositories
 # and return Gitlab::Git::Compare object that responds to commits and diffs
 class CompareService
-  def execute(source_project, source_branch, target_project, target_branch)
+  def execute(source_project, source_branch, target_project, target_branch, straight: false)
     source_commit = source_project.commit(source_branch)
     return unless source_commit
 
@@ -23,9 +23,10 @@ class CompareService
     raw_compare = Gitlab::Git::Compare.new(
       target_project.repository.raw_repository,
       target_branch,
-      source_sha
+      source_sha,
+      straight
     )
 
-    Compare.new(raw_compare, target_project)
+    Compare.new(raw_compare, target_project, straight: straight)
   end
 end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index efeb9df9527f6263aea1c40e76f8bb24dc20f89a..8ae15ad32f4a06dfb384b53f280d9b989a8f001c 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -2,17 +2,72 @@ require_relative 'base_service'
 
 class CreateDeploymentService < BaseService
   def execute(deployable = nil)
-    environment = project.environments.find_or_create_by(
-      name: params[:environment]
-    )
+    return unless executable?
 
+    ActiveRecord::Base.transaction do
+      @deployable = deployable
+
+      @environment = environment
+      @environment.external_url = expanded_url if expanded_url
+      @environment.fire_state_event(action)
+
+      return unless @environment.save
+      return if @environment.stopped?
+
+      deploy.tap do |deployment|
+        deployment.update_merge_request_metrics!
+      end
+    end
+  end
+
+  private
+
+  def executable?
+    project && name.present?
+  end
+
+  def deploy
     project.deployments.create(
-      environment: environment,
+      environment: @environment,
       ref: params[:ref],
       tag: params[:tag],
       sha: params[:sha],
       user: current_user,
-      deployable: deployable
-    )
+      deployable: @deployable,
+      on_stop: options[:on_stop])
+  end
+
+  def environment
+    @environment ||= project.environments.find_or_create_by(name: expanded_name)
+  end
+
+  def expanded_name
+    ExpandVariables.expand(name, variables)
+  end
+
+  def expanded_url
+    return unless url
+
+    @expanded_url ||= ExpandVariables.expand(url, variables)
+  end
+
+  def name
+    params[:environment]
+  end
+
+  def url
+    options[:url]
+  end
+
+  def options
+    params[:options] || {}
+  end
+
+  def variables
+    params[:variables] || []
+  end
+
+  def action
+    options[:action] || 'start'
   end
 end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 918eddaa53a906844ab13dfb9f50b8f186e6ce62..3e5dd4ebb86b90da2322d91b1c27d38095a1a6d2 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -42,7 +42,7 @@ class DeleteBranchService < BaseService
     Gitlab::DataBuilder::Push.build(
       project,
       current_user,
-      branch.target.sha,
+      branch.dereferenced_target.sha,
       Gitlab::Git::BLANK_SHA,
       "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
       [])
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index d0cb151a010cde5cd721514a3068972c49888afb..d824406cb491048f6ee7d9ac9360693fd131c7c0 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -36,7 +36,7 @@ class DeleteTagService < BaseService
     Gitlab::DataBuilder::Push.build(
       project,
       current_user,
-      tag.target.sha,
+      tag.dereferenced_target.sha,
       Gitlab::Git::BLANK_SHA,
       "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
       [])
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 07fc77001a52f9957caee0952e34bd00b05ec3c6..e24cc66e0fe50e042773a75567d598995100ca6e 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -62,6 +62,10 @@ class EventCreateService
     create_event(project, current_user, Event::LEFT)
   end
 
+  def expired_leave_project(project, current_user)
+    create_event(project, current_user, Event::EXPIRED)
+  end
+
   def create_project(project, current_user)
     create_event(project, current_user, Event::CREATED)
   end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index ea94818713bedf82a9ed59c0c9c32ae4a49cd993..9bd4bd464f7940253aeb7fd48014fdae3ae22cb6 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -16,6 +16,8 @@ module Files
                           params[:file_content]
                         end
       @last_commit_sha = params[:last_commit_sha]
+      @author_email    = params[:author_email]
+      @author_name     = params[:author_name]
 
       # Validate parameters
       validate
@@ -25,8 +27,9 @@ module Files
         create_target_branch
       end
 
-      if commit
-        success
+      result = commit
+      if result
+        success(result: result)
       else
         error('Something went wrong. Your changes were not committed')
       end
@@ -40,6 +43,12 @@ module Files
       @source_branch != @target_branch || @source_project != @project
     end
 
+    def file_has_changed?
+      return false unless @last_commit_sha && last_commit
+
+      @last_commit_sha != last_commit.sha
+    end
+
     def raise_error(message)
       raise ValidationError.new(message)
     end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 6107254a34ee01b9bdc4484c22bd0a96b2f1c973..d00d78cee7ee5f724d362b0ff639178983c0b399 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
 module Files
   class CreateDirService < Files::BaseService
     def commit
-      repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
+      repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
     end
 
     def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 8eaf6db8012ebc47a833cbf6c643ff051f58283a..bf127843d55519539e9414ff4a6c12bfe40ffe01 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
 module Files
   class CreateService < Files::BaseService
     def commit
-      repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false)
+      repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
     end
 
     def validate
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 27c881c34308ad1bb360546bbfcee1bd49733d33..8b27ad51789bc54d2f64a160bb09a0fb2d4942c0 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
 module Files
   class DeleteService < Files::BaseService
     def commit
-      repository.remove_file(current_user, @file_path, @commit_message, @target_branch)
+      repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
     end
   end
 end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d28912e1301cdac9f5af947a53bbebb083396495
--- /dev/null
+++ b/app/services/files/multi_service.rb
@@ -0,0 +1,124 @@
+require_relative "base_service"
+
+module Files
+  class MultiService < Files::BaseService
+    class FileChangedError < StandardError; end
+
+    def commit
+      repository.multi_action(
+        user: current_user,
+        branch: @target_branch,
+        message: @commit_message,
+        actions: params[:actions],
+        author_email: @author_email,
+        author_name: @author_name
+      )
+    end
+
+    private
+
+    def validate
+      super
+
+      params[:actions].each_with_index do |action, index|
+        unless action[:file_path].present?
+          raise_error("You must specify a file_path.")
+        end
+
+        regex_check(action[:file_path])
+        regex_check(action[:previous_path]) if action[:previous_path]
+
+        if project.empty_repo? && action[:action] != :create
+          raise_error("No files to #{action[:action]}.")
+        end
+
+        validate_file_exists(action)
+
+        case action[:action]
+        when :create
+          validate_create(action)
+        when :update
+          validate_update(action)
+        when :delete
+          validate_delete(action)
+        when :move
+          validate_move(action, index)
+        else
+          raise_error("Unknown action type `#{action[:action]}`.")
+        end
+      end
+    end
+
+    def validate_file_exists(action)
+      return if action[:action] == :create
+
+      file_path = action[:file_path]
+      file_path = action[:previous_path] if action[:action] == :move
+
+      blob = repository.blob_at_branch(params[:branch_name], file_path)
+
+      unless blob
+        raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+      end
+    end
+
+    def last_commit
+      Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+    end
+
+    def regex_check(file)
+      if file =~ Gitlab::Regex.directory_traversal_regex
+        raise_error(
+          'Your changes could not be committed, because the file name, `' +
+          file +
+          '` ' +
+          Gitlab::Regex.directory_traversal_regex_message
+        )
+      end
+
+      unless file =~ Gitlab::Regex.file_path_regex
+        raise_error(
+          'Your changes could not be committed, because the file name, `' +
+          file +
+          '` ' +
+          Gitlab::Regex.file_path_regex_message
+        )
+      end
+    end
+
+    def validate_create(action)
+      return if project.empty_repo?
+
+      if repository.blob_at_branch(params[:branch_name], action[:file_path])
+        raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
+      end
+    end
+
+    def validate_delete(action)
+    end
+
+    def validate_move(action, index)
+      if action[:previous_path].nil?
+        raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
+      end
+
+      blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+
+      if blob
+        raise_error("Move destination `#{action[:file_path]}` already exists.")
+      end
+
+      if action[:content].nil?
+        blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+        blob.load_all_data!(repository) if blob.truncated?
+        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 4fc3b64079925085e2bbdc437d9e59585a09dce5..c17fdb8d1f11779c1624714a0550b1cdf240977a 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -8,7 +8,9 @@ module Files
       repository.update_file(current_user, @file_path, @file_content,
                              branch: @target_branch,
                              previous_path: @previous_path,
-                             message: @commit_message)
+                             message: @commit_message,
+                             author_email: @author_email,
+                             author_name: @author_name)
     end
 
     private
@@ -21,12 +23,6 @@ module Files
       end
     end
 
-    def file_has_changed?
-      return false unless @last_commit_sha && last_commit
-
-      @last_commit_sha != last_commit.sha
-    end
-
     def last_commit
       @last_commit ||= Gitlab::Git::Commit.
         last_for_path(@source_project.repository, @source_branch, @file_path)
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 78feb37aa2a653ceac90a046fe32e12ddc95ddfb..de313095bedb85b03c950b45a331a6140724be9b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -63,13 +63,12 @@ class GitPushService < BaseService
   protected
 
   def update_merge_requests
-    @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
+    UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
 
     EventCreateService.new.push(@project, current_user, build_push_data)
-    SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
     @project.execute_hooks(build_push_data.dup, :push_hooks)
     @project.execute_services(build_push_data.dup, :push_hooks)
-    Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
+    Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
     ProjectCacheWorker.perform_async(@project.id)
   end
 
@@ -87,7 +86,7 @@ class GitPushService < BaseService
     project.change_head(branch_name)
 
     # Set protection on the default branch if configured
-    if current_application_settings.default_branch_protection != PROTECTION_NONE
+    if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
 
       params = {
         name: @project.default_branch,
@@ -106,34 +105,11 @@ class GitPushService < BaseService
   # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
   # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
   def process_commit_messages
-    is_default_branch = is_default_branch?
-
-    authors = Hash.new do |hash, commit|
-      email = commit.author_email
-      next hash[email] if hash.has_key?(email)
-
-      hash[email] = commit_user(commit)
-    end
+    default = is_default_branch?
 
     @push_commits.each do |commit|
-      # Keep track of the issues that will be actually closed because they are on a default branch.
-      # Hence, when creating cross-reference notes, the not-closed issues (on non-default branches)
-      # will also have cross-reference.
-      closed_issues = []
-
-      if is_default_branch
-        # Close issues if these commits were pushed to the project's default branch and the commit message matches the
-        # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
-        # a different branch.
-        closed_issues = commit.closes_issues(current_user)
-        closed_issues.each do |issue|
-          if can?(current_user, :update_issue, issue)
-            Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
-          end
-        end
-      end
-
-      commit.create_cross_references!(authors[commit], closed_issues)
+      ProcessCommitWorker.
+        perform_async(project.id, current_user.id, commit.id, default)
     end
   end
 
@@ -147,16 +123,6 @@ class GitPushService < BaseService
       push_commits)
   end
 
-  def build_push_data_system_hook
-    @push_data_system ||= Gitlab::DataBuilder::Push.build(
-      @project,
-      current_user,
-      params[:oldrev],
-      params[:newrev],
-      params[:ref],
-      [])
-  end
-
   def push_to_existing_branch?
     # Return if this is not a push to a branch (e.g. new commits)
     Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index e6002b03b933a269592590c97d864eee51506b48..20a4445bddf2334b54be1388b4aec5a1b44a4ec3 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -27,8 +27,8 @@ class GitTagPushService < BaseService
       tag_name = Gitlab::Git.ref_name(params[:ref])
       tag = project.repository.find_tag(tag_name)
 
-      if tag && tag.object_sha == params[:newrev]
-        commit = project.commit(tag.target)
+      if tag && tag.target == params[:newrev]
+        commit = project.commit(tag.dereferenced_target)
         commits = [commit].compact
         message = tag.message
       end
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60891cbb255fbdf6732647ecc84c733d2b228cc6
--- /dev/null
+++ b/app/services/issuable/bulk_update_service.rb
@@ -0,0 +1,26 @@
+module Issuable
+  class BulkUpdateService < IssuableBaseService
+    def execute(type)
+      model_class = type.classify.constantize
+      update_class = type.classify.pluralize.constantize::UpdateService
+
+      ids = params.delete(:issuable_ids).split(",")
+      items = model_class.where(id: ids)
+
+      %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
+        params.delete(key) unless params[key].present?
+      end
+
+      items.each do |issuable|
+        next unless can?(current_user, :"update_#{type}", issuable)
+
+        update_class.new(issuable.project, current_user, params).execute(issuable)
+      end
+
+      {
+        count:    items.count,
+        success:  !items.count.zero?
+      }
+    end
+  end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2d96efe1042c955fc984e0c895ae6ae9e84a6100..bb92cd80cc9bf5df48d7b24d6afb4142e6468f3e 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -45,10 +45,12 @@ class IssuableBaseService < BaseService
 
     unless can?(current_user, ability, project)
       params.delete(:milestone_id)
+      params.delete(:labels)
       params.delete(:add_label_ids)
       params.delete(:remove_label_ids)
       params.delete(:label_ids)
       params.delete(:assignee_id)
+      params.delete(:due_date)
     end
   end
 
@@ -69,46 +71,128 @@ class IssuableBaseService < BaseService
   end
 
   def filter_labels
-    if params[:add_label_ids].present? || params[:remove_label_ids].present?
-      params.delete(:label_ids)
+    filter_labels_in_param(:add_label_ids)
+    filter_labels_in_param(:remove_label_ids)
+    filter_labels_in_param(:label_ids)
+    find_or_create_label_ids
+  end
+
+  def filter_labels_in_param(key)
+    return if params[key].to_a.empty?
+
+    params[key] = available_labels.where(id: params[key]).pluck(:id)
+  end
+
+  def find_or_create_label_ids
+    labels = params.delete(:labels)
+    return unless labels
+
+    params[:label_ids] = labels.split(',').map do |label_name|
+      service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
+      label   = service.execute
+
+      label.id
+    end
+  end
 
-      filter_labels_in_param(:add_label_ids)
-      filter_labels_in_param(:remove_label_ids)
+  def process_label_ids(attributes, existing_label_ids: nil)
+    label_ids = attributes.delete(:label_ids)
+    add_label_ids = attributes.delete(:add_label_ids)
+    remove_label_ids = attributes.delete(:remove_label_ids)
+
+    new_label_ids = existing_label_ids || label_ids || []
+
+    if add_label_ids.blank? && remove_label_ids.blank?
+      new_label_ids = label_ids if label_ids
     else
-      filter_labels_in_param(:label_ids)
+      new_label_ids |= add_label_ids if add_label_ids
+      new_label_ids -= remove_label_ids if remove_label_ids
     end
+
+    new_label_ids
   end
 
-  def filter_labels_in_param(key)
-    return if params[key].to_a.empty?
+  def available_labels
+    LabelsFinder.new(current_user, project_id: @project.id).execute
+  end
 
-    params[key] = project.labels.where(id: params[key]).pluck(:id)
+  def merge_slash_commands_into_params!(issuable)
+    description, command_params =
+      SlashCommands::InterpretService.new(project, current_user).
+      execute(params[:description], issuable)
+
+    params[:description] = description
+
+    params.merge!(command_params)
   end
 
-  def update_issuable(issuable, attributes)
+  def create_issuable(issuable, attributes, label_ids:)
     issuable.with_transaction_returning_status do
-      add_label_ids = attributes.delete(:add_label_ids)
-      remove_label_ids = attributes.delete(:remove_label_ids)
+      if issuable.save
+        issuable.update_attributes(label_ids: label_ids)
+      end
+    end
+  end
 
-      issuable.label_ids |= add_label_ids if add_label_ids
-      issuable.label_ids -= remove_label_ids if remove_label_ids
+  def create(issuable)
+    merge_slash_commands_into_params!(issuable)
+    filter_params
+
+    params.delete(:state_event)
+    params[:author] ||= current_user
+    label_ids = process_label_ids(params)
+
+    issuable.assign_attributes(params)
+
+    before_create(issuable)
+
+    if params.present? && create_issuable(issuable, params, label_ids: label_ids)
+      after_create(issuable)
+      issuable.create_cross_references!(current_user)
+      execute_hooks(issuable)
+    end
+
+    issuable
+  end
+
+  def before_create(issuable)
+    # To be overridden by subclasses
+  end
 
-      issuable.assign_attributes(attributes.merge(updated_by: current_user))
+  def after_create(issuable)
+    # To be overridden by subclasses
+  end
+
+  def after_update(issuable)
+    # To be overridden by subclasses
+  end
 
-      issuable.save
+  def update_issuable(issuable, attributes)
+    issuable.with_transaction_returning_status do
+      issuable.update(attributes.merge(updated_by: current_user))
     end
   end
 
   def update(issuable)
     change_state(issuable)
     change_subscription(issuable)
+    change_todo(issuable)
     filter_params
     old_labels = issuable.labels.to_a
+    old_mentioned_users = issuable.mentioned_users.to_a
+
+    params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
 
     if params.present? && update_issuable(issuable, params)
       issuable.reset_events_cache
-      handle_common_system_notes(issuable, old_labels: old_labels)
-      handle_changes(issuable, old_labels: old_labels)
+
+      # We do not touch as it will affect a update on updated_at field
+      ActiveRecord::Base.no_touching do
+        handle_common_system_notes(issuable, old_labels: old_labels)
+      end
+
+      handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+      after_update(issuable)
       issuable.create_new_cross_references!(current_user)
       execute_hooks(issuable, 'update')
     end
@@ -134,6 +218,16 @@ class IssuableBaseService < BaseService
     end
   end
 
+  def change_todo(issuable)
+    case params.delete(:todo_event)
+    when 'add'
+      todo_service.mark_todo(issuable, current_user)
+    when 'done'
+      todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
+      todo_service.mark_todos_as_done([todo], current_user) if todo
+    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 089b0f527e21262e8ade9957c7e34f83317cbf3d..9ea3ce084bae192d624ee7fa2a0fbfb0c7a22da4 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -14,9 +14,10 @@ module Issues
     end
 
     def execute_hooks(issue, action = 'open')
-      issue_data = hook_data(issue, action)
-      issue.project.execute_hooks(issue_data, :issue_hooks)
-      issue.project.execute_services(issue_data, :issue_hooks)
+      issue_data  = hook_data(issue, action)
+      hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
+      issue.project.execute_hooks(issue_data, hooks_scope)
+      issue.project.execute_services(issue_data, hooks_scope)
     end
   end
 end
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
deleted file mode 100644
index 7e19a73f71a22491f18dff6be77ba9698ce03641..0000000000000000000000000000000000000000
--- a/app/services/issues/bulk_update_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Issues
-  class BulkUpdateService < BaseService
-    def execute
-      issues_ids   = params.delete(:issues_ids).split(",")
-      issue_params = params
-
-      %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
-        issue_params.delete(key) unless issue_params[key].present?
-      end
-
-      issues = Issue.where(id: issues_ids)
-
-      issues.each do |issue|
-        next unless can?(current_user, :update_issue, issue)
-
-        Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
-      end
-
-      {
-        count:    issues.count,
-        success:  !issues.count.zero?
-      }
-    end
-  end
-end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 859c934ea3bf0493195fb6d106bb9c858b4baf13..ab4c51386a42973cbe9f3d1ae3291fd7dbf48de6 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,21 @@
 module Issues
   class CloseService < Issues::BaseService
+    # Closes the supplied issue if the current user is able to do so.
     def execute(issue, commit: nil, notifications: true, system_note: true)
+      return issue unless can?(current_user, :update_issue, issue)
+
+      close_issue(issue,
+                  commit: commit,
+                  notifications: notifications,
+                  system_note: system_note)
+    end
+
+    # Closes the supplied issue without checking if the user is authorized to
+    # do so.
+    #
+    # The code calling this method is responsible for ensuring that a user is
+    # allowed to close the given issue.
+    def close_issue(issue, commit: nil, notifications: true, system_note: true)
       if project.jira_tracker? && project.jira_service.active
         project.jira_service.execute(commit, issue)
         todo_service.close_issue(issue, current_user)
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 65550ab8ec6cd5160fd5d88be71a9fb3c7f42640..ea1690f3e381b630425225059dfcb1074a77a5a5 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,26 +1,23 @@
 module Issues
   class CreateService < Issues::BaseService
     def execute
-      filter_params
-      label_params = params.delete(:label_ids)
       @request = params.delete(:request)
       @api = params.delete(:api)
-      @issue = project.issues.new(params)
-      @issue.author = params[:author] || current_user
 
-      @issue.spam = spam_service.check(@api)
+      @issue = project.issues.new
 
-      if @issue.save
-        @issue.update_attributes(label_ids: label_params)
-        notification_service.new_issue(@issue, current_user)
-        todo_service.new_issue(@issue, current_user)
-        event_service.open_issue(@issue, current_user)
-        user_agent_detail_service.create
-        @issue.create_cross_references!(current_user)
-        execute_hooks(@issue, 'open')
-      end
+      create(@issue)
+    end
+
+    def before_create(issuable)
+      issuable.spam = spam_service.check(@api)
+    end
 
-      @issue
+    def after_create(issuable)
+      event_service.open_issue(issuable, current_user)
+      notification_service.new_issue(issuable, current_user)
+      todo_service.new_issue(issuable, current_user)
+      user_agent_detail_service.create
     end
 
     private
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index ab667456db772ba02de888860d307acaa918b264..a2a5f57d069db4907b1d34fb41b3f61b5106363a 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -52,8 +52,12 @@ module Issues
     end
 
     def cloneable_label_ids
-      @new_project.labels
-        .where(title: @old_issue.labels.pluck(:title)).pluck(:id)
+      params = {
+        project_id: @new_project.id,
+        title: @old_issue.labels.pluck(:title)
+      }
+
+      LabelsFinder.new(current_user, params).execute.pluck(:id)
     end
 
     def cloneable_milestone_id
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index e48ca359f4f9905fbcb169792611b885f0529f81..40fbe354492842ecc67d26710c3b717f0e7c946c 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -1,6 +1,8 @@
 module Issues
   class ReopenService < Issues::BaseService
     def execute(issue)
+      return issue unless can?(current_user, :update_issue, issue)
+
       if issue.reopen
         event_service.reopen_issue(issue, current_user)
         create_note(issue)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c7d406cc3317ba8e0a904a99ff5dca41843dfb3d..a2111b3806ba7fbc710315feb762dbe68e389d76 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,7 @@ module Issues
       update(issue)
     end
 
-    def handle_changes(issue, old_labels: [])
+    def handle_changes(issue, old_labels: [], old_mentioned_users: [])
       if has_changes?(issue, old_labels: old_labels)
         todo_service.mark_pending_todos_as_done(issue, current_user)
       end
@@ -32,6 +32,11 @@ module Issues
       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
     end
 
     def reopen_service
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d622f9edd3334da3b208b95f6fee942115fc524e
--- /dev/null
+++ b/app/services/labels/find_or_create_service.rb
@@ -0,0 +1,36 @@
+module Labels
+  class FindOrCreateService
+    def initialize(current_user, project, params = {})
+      @current_user = current_user
+      @project = project
+      @params = params.dup
+    end
+
+    def execute(skip_authorization: false)
+      @skip_authorization = skip_authorization
+      find_or_create_label
+    end
+
+    private
+
+    attr_reader :current_user, :project, :params, :skip_authorization
+
+    def available_labels
+      @available_labels ||= LabelsFinder.new(
+        current_user,
+        project_id: project.id
+      ).execute(skip_authorization: skip_authorization)
+    end
+
+    def find_or_create_label
+      new_label = available_labels.find_by(title: title)
+      new_label ||= project.labels.create(params)
+
+      new_label
+    end
+
+    def title
+      params[:title] || params[:name]
+    end
+  end
+end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..514679ed29d02d6450a60a1194901e9ddab2cf88
--- /dev/null
+++ b/app/services/labels/transfer_service.rb
@@ -0,0 +1,78 @@
+# Labels::TransferService class
+#
+# User for recreate the missing group labels at project level
+#
+module Labels
+  class TransferService
+    def initialize(current_user, old_group, project)
+      @current_user = current_user
+      @old_group = old_group
+      @project = project
+    end
+
+    def execute
+      return unless old_group.present?
+
+      Label.transaction do
+        labels_to_transfer.find_each do |label|
+          new_label_id = find_or_create_label!(label)
+
+          next if new_label_id == label.id
+
+          update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
+          update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
+          update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
+        end
+      end
+    end
+
+    private
+
+    attr_reader :current_user, :old_group, :project
+
+    def labels_to_transfer
+      label_ids = []
+      label_ids << group_labels_applied_to_issues.select(:id)
+      label_ids << group_labels_applied_to_merge_requests.select(:id)
+
+      union = Gitlab::SQL::Union.new(label_ids)
+
+      Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
+    end
+
+    def group_labels_applied_to_issues
+      Label.joins(:issues).
+        where(
+          issues: { project_id: project.id },
+          labels: { type: 'GroupLabel', group_id: old_group.id }
+        )
+    end
+
+    def group_labels_applied_to_merge_requests
+      Label.joins(:merge_requests).
+        where(
+          merge_requests: { target_project_id: project.id },
+          labels: { type: 'GroupLabel', group_id: old_group.id }
+        )
+    end
+
+    def find_or_create_label!(label)
+      params    = label.attributes.slice('title', 'description', 'color')
+      new_label = FindOrCreateService.new(current_user, project, params).execute
+
+      new_label.id
+    end
+
+    def update_label_links(labels, old_label_id:, new_label_id:)
+      LabelLink.joins(:label).
+        merge(labels).
+        where(label_id: old_label_id).
+        update_all(label_id: new_label_id)
+    end
+
+    def update_label_priorities(old_label_id:, new_label_id:)
+      LabelPriority.where(project_id: project.id, label_id: old_label_id).
+        update_all(label_id: new_label_id)
+    end
+  end
+end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c13f289f61e1bd33de0ade3bb2b7da116fda05fc
--- /dev/null
+++ b/app/services/members/approve_access_request_service.rb
@@ -0,0 +1,42 @@
+module Members
+  class ApproveAccessRequestService < BaseService
+    include MembersHelper
+
+    attr_accessor :source
+
+    # source - The source object that respond to `#requesters` (i.g. project or group)
+    # current_user - The user that performs the access request approval
+    # params - A hash of parameters
+    #   :user_id - User ID used to retrieve the access requester
+    #   :id - Member ID used to retrieve the access requester
+    #   :access_level - Optional access level set when the request is accepted
+    def initialize(source, current_user, params = {})
+      @source = source
+      @current_user = current_user
+      @params = params.slice(:user_id, :id, :access_level)
+    end
+
+    # opts - A hash of options
+    #   :force - Bypass permission check: current_user can be nil in that case
+    def execute(opts = {})
+      condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
+      access_requester = source.requesters.find_by!(condition)
+
+      raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
+
+      access_requester.access_level = params[:access_level] if params[:access_level]
+      access_requester.accept_request
+
+      access_requester
+    end
+
+    private
+
+    def can_update_access_requester?(access_requester, opts = {})
+      access_requester && (
+        opts[:force] ||
+        can?(current_user, action_member_permission(:update, access_requester), access_requester)
+      )
+    end
+  end
+end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7a244c2029282cc8ad8cdd720e35414a8bd1926
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,21 @@
+module Members
+  class AuthorizedDestroyService < BaseService
+    attr_accessor :member, :user
+
+    def initialize(member, user = nil)
+      @member, @user = member, user
+    end
+
+    def execute
+      return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+      member.destroy
+
+      if member.request? && member.user != user
+        notification_service.decline_access_request(member)
+      end
+
+      member
+    end
+  end
+end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4b24ccef92c834436f25d04da4f792cc80ae8ae
--- /dev/null
+++ b/app/services/members/create_service.rb
@@ -0,0 +1,16 @@
+module Members
+  class CreateService < BaseService
+    def execute
+      return false if params[:user_ids].blank?
+
+      project.team.add_users(
+        params[:user_ids].split(','),
+        params[:access_level],
+        expires_at: params[:expires_at],
+        current_user: current_user
+      )
+
+      true
+    end
+  end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9e3f6af628daa49565ac9e56fd8b9d215906b4dd..431da8372c96d0403ecf7c562999daa65dc9c524 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -1,22 +1,42 @@
 module Members
   class DestroyService < BaseService
-    attr_accessor :member, :current_user
+    include MembersHelper
 
-    def initialize(member, current_user)
-      @member = member
+    attr_accessor :source
+
+    ALLOWED_SCOPES = %i[members requesters all]
+
+    def initialize(source, current_user, params = {})
+      @source = source
       @current_user = current_user
+      @params = params
     end
 
-    def execute
-      unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
-        raise Gitlab::Access::AccessDeniedError
-      end
+    def execute(scope = :members)
+      raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
 
-      member.destroy
+      member = find_member!(scope)
 
-      if member.request? && member.user != current_user
-        notification_service.decline_access_request(member)
+      raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
+
+      AuthorizedDestroyService.new(member, current_user).execute
+    end
+
+    private
+
+    def find_member!(scope)
+      condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
+      case scope
+      when :all
+        source.members.find_by(condition) ||
+          source.requesters.find_by!(condition)
+      else
+        source.public_send(scope).find_by!(condition)
       end
     end
+
+    def can_destroy_member?(member)
+      member && can?(current_user, action_member_permission(:destroy, member), member)
+    end
   end
 end
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2614153d9006b337d9d8ed8f43b0ef6b861a5011
--- /dev/null
+++ b/app/services/members/request_access_service.rb
@@ -0,0 +1,25 @@
+module Members
+  class RequestAccessService < BaseService
+    attr_accessor :source
+
+    def initialize(source, current_user)
+      @source = source
+      @current_user = current_user
+    end
+
+    def execute
+      raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
+
+      source.members.create(
+        access_level: Gitlab::Access::DEVELOPER,
+        user: current_user,
+        requested_at: Time.now.utc)
+    end
+
+    private
+
+    def can_request_access?(source)
+      source && can?(current_user, :request_access, source)
+    end
+  end
+end
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 566049525cb724ee4682780f7d9f5791071d0c00..d572a928a42e580a7bf610280d9ed9b4ca66dfb6 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -2,14 +2,14 @@ module MergeRequests
   class AddTodoWhenBuildFailsService < MergeRequests::BaseService
     # Adds a todo to the parent merge_request when a CI build fails
     def execute(commit_status)
-      each_merge_request(commit_status) do |merge_request|
+      commit_status_merge_requests(commit_status) do |merge_request|
         todo_service.merge_request_build_failed(merge_request)
       end
     end
 
     # Closes any pending build failed todos for the parent MRs when a build is retried
     def close(commit_status)
-      each_merge_request(commit_status) do |merge_request|
+      commit_status_merge_requests(commit_status) do |merge_request|
         todo_service.merge_request_build_retried(merge_request)
       end
     end
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..066efa1acc3dd7dae9b345dc384794424cbae448
--- /dev/null
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -0,0 +1,35 @@
+module MergeRequests
+  class AssignIssuesService < BaseService
+    def assignable_issues
+      @assignable_issues ||= begin
+        if current_user == merge_request.author
+          closes_issues.select do |issue|
+            !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+          end
+        else
+          []
+        end
+      end
+    end
+
+    def execute
+      assignable_issues.each do |issue|
+        Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
+      end
+
+      {
+        count: assignable_issues.count
+      }
+    end
+
+    private
+
+    def merge_request
+      params[:merge_request]
+    end
+
+    def closes_issues
+      @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user)
+    end
+  end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index ba424b09463a83f7116ce8a6c80d4db44e9a8c34..58f69a41e14bb4d5281dd8a6a7085e1e84342caa 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -5,16 +5,17 @@ module MergeRequests
     end
 
     def create_title_change_note(issuable, old_title)
-      removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
-      added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
+      removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
+      added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
+      changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
 
       if removed_wip
         SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
       elsif added_wip
         SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
-      else
-        super
       end
+
+      super if changed_title
     end
 
     def hook_data(merge_request, action, oldrev = nil)
@@ -41,28 +42,33 @@ module MergeRequests
       super(:merge_request)
     end
 
-    def merge_request_from(commit_status)
-      branches = commit_status.ref
+    def merge_requests_for(branch)
+      origin_merge_requests = @project.origin_merge_requests
+        .opened.where(source_branch: branch).to_a
 
-      # This is for ref-less builds
-      branches ||= @project.repository.branch_names_contains(commit_status.sha)
+      fork_merge_requests = @project.fork_merge_requests
+        .opened.where(source_branch: branch).to_a
 
-      return [] if branches.blank?
+      (origin_merge_requests + fork_merge_requests)
+        .uniq.select(&:source_project)
+    end
 
-      merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
-      merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
+    def pipeline_merge_requests(pipeline)
+      merge_requests_for(pipeline.ref).each do |merge_request|
+        next unless pipeline == merge_request.pipeline
 
-      merge_requests.uniq.select(&:source_project)
+        yield merge_request
+      end
     end
 
-    def each_merge_request(commit_status)
-      merge_request_from(commit_status).each do |merge_request|
+    def commit_status_merge_requests(commit_status)
+      merge_requests_for(commit_status.ref).each do |merge_request|
         pipeline = merge_request.pipeline
 
         next unless pipeline
         next unless pipeline.sha == commit_status.sha
 
-        yield merge_request, pipeline
+        yield merge_request
       end
     end
   end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 290742f1506dda44fa2dda5c6add2d888b900731..f415244068b4345edd9d5640f4b7a87e5d0d8a4f 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
       merge_request = MergeRequest.new(params)
 
       # Set MR attributes
-      merge_request.can_be_created = false
+      merge_request.can_be_created = true
       merge_request.compare_commits = []
       merge_request.source_project = project unless merge_request.source_project
 
@@ -13,14 +13,8 @@ module MergeRequests
       merge_request.target_project ||= (project.forked_from_project || project)
       merge_request.target_branch ||= merge_request.target_project.default_branch
 
-      if merge_request.target_branch.blank? || merge_request.source_branch.blank?
-        message =
-          if params[:source_branch] || params[:target_branch]
-            "You must select source and target branch"
-          end
-
-        return build_failed(merge_request, message)
-      end
+      messages = validate_branches(merge_request)
+      return build_failed(merge_request, messages) unless messages.empty?
 
       compare = CompareService.new.execute(
         merge_request.source_project,
@@ -29,23 +23,42 @@ module MergeRequests
         merge_request.target_branch,
       )
 
-      commits = compare.commits
-
-      # At this point we decide if merge request can be created
-      # If we have at least one commit to merge -> creation allowed
-      if commits.present?
-        merge_request.compare_commits = commits
-        merge_request.can_be_created = true
-        merge_request.compare = compare
-      else
-        merge_request.can_be_created = false
-      end
+      merge_request.compare_commits = compare.commits
+      merge_request.compare = compare
 
       set_title_and_description(merge_request)
     end
 
     private
 
+    def validate_branches(merge_request)
+      messages = []
+
+      if merge_request.target_branch.blank? || merge_request.source_branch.blank?
+        messages <<
+          if params[:source_branch] || params[:target_branch]
+            "You must select source and target branch"
+          end
+      end
+
+      if merge_request.source_project == merge_request.target_project &&
+         merge_request.target_branch == merge_request.source_branch
+
+        messages << 'You must select different branches'
+      end
+
+      # See if source and target branches exist
+      unless merge_request.source_project.commit(merge_request.source_branch)
+        messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
+      end
+
+      unless merge_request.target_project.commit(merge_request.target_branch)
+        messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
+      end
+
+      messages
+    end
+
     # When your branch name starts with an iid followed by a dash this pattern will be
     # interpreted as the user wants to close that issue on this project.
     #
@@ -83,17 +96,21 @@ module MergeRequests
         closes_issue = "Closes ##{iid}"
 
         if merge_request.description.present?
-          merge_request.description += closes_issue.prepend("\n")
+          merge_request.description += closes_issue.prepend("\n\n")
         else
           merge_request.description = closes_issue
         end
       end
 
+      merge_request.title = merge_request.wip_title if commits.empty?
+
       merge_request
     end
 
-    def build_failed(merge_request, message)
-      merge_request.errors.add(:base, message) unless message.nil?
+    def build_failed(merge_request, messages)
+      messages.compact.each do |message|
+        merge_request.errors.add(:base, message)
+      end
       merge_request.compare_commits = []
       merge_request.can_be_created = false
       merge_request
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 27ee81fe3e753d8fe572b8c12b4fa61e6e8f4aed..f2053bda83aadbb4dbcb8766999f404d02b6c7bc 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -1,6 +1,8 @@
 module MergeRequests
   class CloseService < MergeRequests::BaseService
     def execute(merge_request, commit = nil)
+      return merge_request unless can?(current_user, :update_merge_request, merge_request)
+
       # If we close MergeRequest we want to ignore validation
       # so we can close broken one (Ex. fork project removed)
       merge_request.allow_broken = true
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 96a25330af100447755e284febaa6ffdf5028ad4..b0ae2dfe4ce532a9d6f76bbdd54dc926e5f6098e 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -7,26 +7,20 @@ module MergeRequests
       source_project = @project
       @project = Project.find(params[:target_project_id]) if params[:target_project_id]
 
-      filter_params
-      label_params = params.delete(:label_ids)
-      force_remove_source_branch = params.delete(:force_remove_source_branch)
+      params[:target_project_id] ||= source_project.id
 
-      merge_request = MergeRequest.new(params)
+      merge_request = MergeRequest.new
       merge_request.source_project = source_project
-      merge_request.target_project ||= source_project
-      merge_request.author = current_user
-      merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
+      merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
 
-      if merge_request.save
-        merge_request.update_attributes(label_ids: label_params)
-        event_service.open_mr(merge_request, current_user)
-        notification_service.new_merge_request(merge_request, current_user)
-        todo_service.new_merge_request(merge_request, current_user)
-        merge_request.create_cross_references!(current_user)
-        execute_hooks(merge_request)
-      end
+      create(merge_request)
+    end
 
-      merge_request
+    def after_create(issuable)
+      event_service.open_mr(issuable, current_user)
+      notification_service.new_merge_request(issuable, current_user)
+      todo_service.new_merge_request(issuable, current_user)
+      issuable.cache_merge_request_closes_issues!(current_user)
     end
   end
 end
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 08c1f72d65a1190525a31f14dd49ef0421256b74..1262ecbc29aa00a9ace6ac63244ba2846e7ee693 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -31,7 +31,7 @@ module MergeRequests
 
     def get_branches(changes)
       return [] if project.empty_repo?
-      return [] unless project.merge_requests_enabled
+      return [] unless project.merge_requests_enabled?
 
       changes_list = Gitlab::ChangesList.new(changes)
       changes_list.map do |change|
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b037780c431624f8ca9e0ed4e6abe90265bed696..ab9056a32508b54335b2d8632f0f242438d07a4b 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -11,14 +11,14 @@ module MergeRequests
     def execute(merge_request)
       @merge_request = merge_request
 
-      return error('Merge request is not mergeable') unless @merge_request.mergeable?
+      return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
 
       merge_request.in_locked_state do
         if commit
           after_merge
           success
         else
-          error('Can not merge changes')
+          log_merge_error('Can not merge changes', true)
         end
       end
     end
@@ -46,8 +46,8 @@ module MergeRequests
       merge_request.update(merge_error: e.message)
       false
     rescue StandardError => e
-      merge_request.update(merge_error: "Something went wrong during merge")
-      Rails.logger.error(e.message)
+      merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
+      log_merge_error(e.message)
       false
     ensure
       merge_request.update(in_progress_merge_commit_sha: nil)
@@ -65,5 +65,17 @@ module MergeRequests
     def branch_deletion_user
       @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
     end
+
+    def log_merge_error(message, http_error = false)
+      Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
+
+      error(message) if http_error
+    end
+
+    def merge_request_info
+      project = merge_request.project
+
+      "#{project.to_reference}#{merge_request.to_reference}"
+    end
   end
 end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 4ad5fb083114400d4b4bd8aef1a573e9cd71227f..dc159de00581d2dd9608db3794b7cc99ea1f99fe 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -18,12 +18,13 @@ module MergeRequests
       merge_request.save
     end
 
-    # Triggers the automatic merge of merge_request once the build succeeds
-    def trigger(commit_status)
-      each_merge_request(commit_status) do |merge_request, pipeline|
+    # Triggers the automatic merge of merge_request once the pipeline succeeds
+    def trigger(pipeline)
+      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 pipeline.success?
 
         MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
       end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8437d9b8b439e046911d4860903bcb0f21e7eae0..e8fb1b597527cb1088988cbefff626bec2cdff94 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -7,6 +7,7 @@ module MergeRequests
   class PostMergeService < MergeRequests::BaseService
     def execute(merge_request)
       close_issues(merge_request)
+      todo_service.merge_merge_request(merge_request, current_user)
       merge_request.mark_as_merged
       create_merge_event(merge_request, current_user)
       create_note(merge_request)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 5cedd6f11d9e06c9b8869889c0708edc947ecfdf..22596b4014ab3c77c62634139a2a10a0f3bd09fd 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
       reload_merge_requests
       reset_merge_when_build_succeeds
       mark_pending_todos_done
+      cache_merge_requests_closing_issues
 
       # Leave a system note if a branch was deleted/added
       if branch_added? || branch_removed?
@@ -141,6 +142,14 @@ module MergeRequests
       end
     end
 
+    # If the merge requests closes any issues, save this information in the
+    # `MergeRequestsClosingIssues` model (as a performance optimization).
+    def cache_merge_requests_closing_issues
+      @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+        merge_request.cache_merge_request_closes_issues!(@current_user)
+      end
+    end
+
     def filter_merge_requests(merge_requests)
       merge_requests.uniq.select(&:source_project)
     end
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index eb88ae9d11c842e536c3a2e8605f4ae13d3629aa..fadcce5d9b6e13f27e68d8a3433cc471fc1a6eea 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -1,6 +1,8 @@
 module MergeRequests
   class ReopenService < MergeRequests::BaseService
     def execute(merge_request)
+      return merge_request unless can?(current_user, :update_merge_request, merge_request)
+
       if merge_request.reopen
         event_service.reopen_mr(merge_request, current_user)
         create_note(merge_request)
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d22a1d3e0ad3d2c2f797080c41368d4d70074da9
--- /dev/null
+++ b/app/services/merge_requests/resolve_service.rb
@@ -0,0 +1,66 @@
+module MergeRequests
+  class ResolveService < MergeRequests::BaseService
+    class MissingFiles < Gitlab::Conflict::ResolutionError
+    end
+
+    attr_accessor :conflicts, :rugged, :merge_index, :merge_request
+
+    def execute(merge_request)
+      @conflicts = merge_request.conflicts
+      @rugged = project.repository.rugged
+      @merge_index = conflicts.merge_index
+      @merge_request = merge_request
+
+      fetch_their_commit!
+
+      params[:files].each do |file_params|
+        conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
+
+        write_resolved_file_to_index(conflict_file, file_params)
+      end
+
+      unless merge_index.conflicts.empty?
+        missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
+
+        raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
+      end
+
+      commit_params = {
+        message: params[:commit_message] || conflicts.default_commit_message,
+        parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
+        tree: merge_index.write_tree(rugged)
+      }
+
+      project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
+    end
+
+    def write_resolved_file_to_index(file, params)
+      new_file = if params[:sections]
+                   file.resolve_lines(params[:sections]).map(&:text).join("\n")
+                 elsif params[:content]
+                   file.resolve_content(params[:content])
+                 end
+
+      our_path = file.our_path
+
+      merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
+      merge_index.conflict_remove(our_path)
+    end
+
+    # If their commit (in the target project) doesn't exist in the source project, it
+    # can't be a parent for the merge commit we're about to create. If that's the case,
+    # fetch the target branch ref into the source project so the commit exists in both.
+    #
+    def fetch_their_commit!
+      return if rugged.include?(conflicts.their_commit.oid)
+
+      random_string = SecureRandom.hex
+
+      project.repository.fetch_ref(
+        merge_request.target_project.repository.path_to_repo,
+        "refs/heads/#{merge_request.target_branch}",
+        "refs/tmp/#{random_string}/head"
+      )
+    end
+  end
+end
diff --git a/app/services/merge_requests/resolved_discussion_notification_service.rb b/app/services/merge_requests/resolved_discussion_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a09350c84704f0065c9c025f675fc429f8bd153
--- /dev/null
+++ b/app/services/merge_requests/resolved_discussion_notification_service.rb
@@ -0,0 +1,10 @@
+module MergeRequests
+  class ResolvedDiscussionNotificationService < MergeRequests::BaseService
+    def execute(merge_request)
+      return unless merge_request.discussions_resolved?
+
+      SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
+      notification_service.resolve_all_discussions(merge_request, current_user)
+    end
+  end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 026a37997d44376f66bc02c14a3b4643f894dbf9..a37cc3fdf21523055949454c45eda950dc641b05 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,12 +11,19 @@ module MergeRequests
       params.except!(:target_project_id)
       params.except!(:source_branch)
 
-      merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+      if merge_request.closed_without_fork?
+        params.except!(:target_branch, :force_remove_source_branch)
+      end
+
+      if params[:force_remove_source_branch].present?
+        merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+      end
 
+      handle_wip_event(merge_request)
       update(merge_request)
     end
 
-    def handle_changes(merge_request, old_labels: [])
+    def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
       if has_changes?(merge_request, old_labels: old_labels)
         todo_service.mark_pending_todos_as_done(merge_request, current_user)
       end
@@ -55,6 +62,15 @@ module MergeRequests
           current_user
         )
       end
+
+      added_mentions = merge_request.mentioned_users - old_mentioned_users
+      if added_mentions.present?
+        notification_service.new_mentions_in_merge_request(
+          merge_request,
+          added_mentions,
+          current_user
+        )
+      end
     end
 
     def reopen_service
@@ -64,5 +80,22 @@ module MergeRequests
     def close_service
       MergeRequests::CloseService
     end
+
+    def after_update(issuable)
+      issuable.cache_merge_request_closes_issues!(current_user)
+    end
+
+    private
+
+    def handle_wip_event(merge_request)
+      if wip_event = params.delete(:wip_event)
+        # We update the title that is provided in the params or we use the mr title
+        title = params[:title] || merge_request.title
+        params[:title] = case wip_event
+                         when 'wip' then MergeRequest.wip_title(title)
+                         when 'unwip' then MergeRequest.wipless_title(title)
+                         end
+      end
+    end
   end
 end
diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb
index 3b90399af6439816ab5ca3d7e1fa728ebff226fd..b8e08c9f1eb167e97f496f359819b5c013c1aeb5 100644
--- a/app/services/milestones/create_service.rb
+++ b/app/services/milestones/create_service.rb
@@ -3,7 +3,7 @@ module Milestones
     def execute
       milestone = project.milestones.new(params)
 
-      if milestone.save!
+      if milestone.save
         event_service.open_milestone(milestone, current_user)
       end
 
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 18971bd0be3eec0572bbf2b23b8a9a5570f52012..723cc0e6834f7af3d792495ac3875f016e7404d4 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -7,14 +7,39 @@ module Notes
 
       if note.award_emoji?
         noteable = note.noteable
-        todo_service.new_award_emoji(noteable, current_user)
-        return noteable.create_award_emoji(note.award_emoji_name, current_user)
+        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
 
-      if note.save
+      # 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!
+      slash_commands_service = SlashCommandsService.new(project, current_user)
+
+      if slash_commands_service.supported?(note)
+        content, command_params = slash_commands_service.extract_commands(note)
+
+        only_commands = content.empty?
+
+        note.note = content
+      end
+
+      if !only_commands && note.save
         # Finish the harder work in the background
         NewNoteWorker.perform_in(2.seconds, note.id, params)
-        TodoService.new.new_note(note, current_user)
+        todo_service.new_note(note, current_user)
+      end
+
+      if command_params && command_params.any?
+        slash_commands_service.execute(command_params, note)
+
+        # We must add the error after we call #save because errors are reset
+        # when #save is called
+        if only_commands
+          note.errors.add(:commands_only, 'Your commands have been executed!')
+        end
       end
 
       note
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2edbd39a9e7effa4787e759a5e79827301399c02
--- /dev/null
+++ b/app/services/notes/slash_commands_service.rb
@@ -0,0 +1,36 @@
+module Notes
+  class SlashCommandsService < BaseService
+    UPDATE_SERVICES = {
+      'Issue' => Issues::UpdateService,
+      'MergeRequest' => MergeRequests::UpdateService
+    }
+
+    def self.noteable_update_service(note)
+      UPDATE_SERVICES[note.noteable_type]
+    end
+
+    def self.supported?(note, current_user)
+      noteable_update_service(note) &&
+        current_user &&
+        current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+    end
+
+    def supported?(note)
+      self.class.supported?(note, current_user)
+    end
+
+    def extract_commands(note)
+      return [note.note, {}] unless supported?(note)
+
+      SlashCommands::InterpretService.new(project, current_user).
+        execute(note.note, note.noteable)
+    end
+
+    def execute(command_params, note)
+      return if command_params.empty?
+      return unless supported?(note)
+
+      self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
+    end
+  end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ab6e51209eeae9828bcd47d43bc26b017aa451b8..6697840cc26e6f9db64c4ef5d74a15c89ba2d669 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -35,6 +35,20 @@ class NotificationService
     new_resource_email(issue, issue.project, :new_issue_email)
   end
 
+  # When issue text is updated, we should send an email to:
+  #
+  #  * newly mentioned project team members with notification level higher than Participating
+  #
+  def new_mentions_in_issue(issue, new_mentioned_users, current_user)
+    new_mentions_in_resource_email(
+      issue,
+      issue.project,
+      new_mentioned_users,
+      current_user,
+      :new_mention_in_issue_email
+    )
+  end
+
   # When we close an issue we should send an email to:
   #
   #  * issue author if their notification level is not Disabled
@@ -75,6 +89,20 @@ class NotificationService
     new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
   end
 
+  # When merge request text is updated, we should send an email to:
+  #
+  #  * newly mentioned project team members with notification level higher than Participating
+  #
+  def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
+    new_mentions_in_resource_email(
+      merge_request,
+      merge_request.target_project,
+      new_mentioned_users,
+      current_user,
+      :new_mention_in_merge_request_email
+    )
+  end
+
   # When we reassign a merge_request we should send an email to:
   #
   #  * merge_request old assignee if their notification level is not Disabled
@@ -106,7 +134,8 @@ class NotificationService
       merge_request,
       merge_request.target_project,
       current_user,
-      :merged_merge_request_email
+      :merged_merge_request_email,
+      skip_current_user: !merge_request.merge_when_build_succeeds?
     )
   end
 
@@ -120,6 +149,14 @@ 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.each do |recipient|
+      mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
+    end
+  end
+
   # Notify new user with email after creation
   def new_user(user, token = nil)
     # Don't email omniauth created users
@@ -177,7 +214,7 @@ class NotificationService
 
     # build notify method like 'note_commit_email'
     notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
-    
+
     recipients.each do |recipient|
       mailer.send(notify_method, recipient.id, note.id).deliver_later
     end
@@ -206,7 +243,6 @@ class NotificationService
       project_member.real_source_type,
       project_member.project.id,
       project_member.invite_email,
-      project_member.access_level,
       project_member.created_by_id
     ).deliver_later
   end
@@ -233,7 +269,6 @@ class NotificationService
       group_member.real_source_type,
       group_member.group.id,
       group_member.invite_email,
-      group_member.access_level,
       group_member.created_by_id
     ).deliver_later
   end
@@ -277,6 +312,22 @@ class NotificationService
     mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
   end
 
+  def pipeline_finished(pipeline, recipients = nil)
+    email_template = "pipeline_#{pipeline.status}_email"
+
+    return unless mailer.respond_to?(email_template)
+
+    recipients ||= build_recipients(
+      pipeline,
+      pipeline.project,
+      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
+    end
+  end
+
   protected
 
   # Get project/group users with CUSTOM notification level
@@ -440,10 +491,17 @@ class NotificationService
   end
 
   def reject_users_without_access(recipients, target)
-    return recipients unless target.is_a?(Issue)
+    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?(:read_issue, target)
+      user.can?(ability, target)
     end
   end
 
@@ -471,9 +529,25 @@ class NotificationService
     end
   end
 
-  def close_resource_email(target, project, current_user, method)
+  def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
+    recipients = build_recipients(target, project, current_user, action: "new")
+    recipients = recipients & new_mentioned_users
+
+    recipients.each do |recipient|
+      mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
+    end
+  end
+
+  def close_resource_email(target, project, current_user, method, skip_current_user: true)
     action = method == :merged_merge_request_email ? "merge" : "close"
-    recipients = build_recipients(target, project, current_user, action: action)
+
+    recipients = build_recipients(
+      target,
+      project,
+      current_user,
+      action: action,
+      skip_current_user: skip_current_user
+    )
 
     recipients.each do |recipient|
       mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -514,7 +588,7 @@ class NotificationService
     end
   end
 
-  def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
+  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)
@@ -543,7 +617,8 @@ class NotificationService
     recipients = reject_unsubscribed_users(recipients, target)
     recipients = reject_users_without_access(recipients, target)
 
-    recipients.delete(current_user)
+    recipients.delete(current_user) if skip_current_user
+
     recipients.uniq
   end
 
@@ -570,6 +645,6 @@ class NotificationService
   # Build event key to search on custom notification level
   # Check NotificationSetting::EMAIL_EVENTS
   def build_custom_key(action, object)
-    "#{action}_#{object.class.name.underscore}".to_sym
+    "#{action}_#{object.class.model_name.name.underscore}".to_sym
   end
 end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 23b6668e0d1c9de43a4fec73ddd5c8486ab56458..015f282892117513f8999f2c0e27de40d8a46483 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,7 +1,7 @@
 module Projects
   class AutocompleteService < BaseService
     def issues
-      @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
+      IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
     end
 
     def milestones
@@ -9,11 +9,34 @@ module Projects
     end
 
     def merge_requests
-      @project.merge_requests.opened.select([:iid, :title])
+      MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
     end
 
     def labels
-      @project.labels.select([:title, :color])
+      LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
+    end
+
+    def commands(noteable, type)
+      noteable ||=
+        case type
+        when 'Issue'
+          @project.issues.build
+        when 'MergeRequest'
+          @project.merge_requests.build
+        end
+
+      return [] unless noteable && noteable.is_a?(Issuable)
+
+      opts = {
+        project: project,
+        issuable: noteable,
+        current_user: current_user
+      }
+      SlashCommands::InterpretService.command_definitions.map do |definition|
+        next unless definition.available?(opts)
+
+        definition.to_h(opts)
+      end.compact
     end
   end
 end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 55956be28445b5b8da27b54d8b1e809ffecd0250..15d7918e7fd18f71e5251112421ab69aa66b90f6 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -7,6 +7,7 @@ module Projects
     def execute
       forked_from_project_id = params.delete(:forked_from_project_id)
       import_data = params.delete(:import_data)
+      @skip_wiki = params.delete(:skip_wiki)
 
       @project = Project.new(params)
 
@@ -16,6 +17,11 @@ module Projects
         return @project
       end
 
+      unless allowed_fork?(forked_from_project_id)
+        @project.errors.add(:forked_from_project_id, 'is forbidden')
+        return @project
+      end
+
       # Set project name from path
       if @project.name.present? && @project.path.present?
         # if both name and path set - everything is ok
@@ -72,6 +78,13 @@ module Projects
       @project.errors.add(:namespace, "is not valid")
     end
 
+    def allowed_fork?(source_project_id)
+      return true if source_project_id.nil?
+
+      source_project = Project.find_by(id: source_project_id)
+      current_user.can?(:fork_project, source_project)
+    end
+
     def allowed_namespace?(user, namespace_id)
       namespace = Namespace.find_by(id: namespace_id)
       current_user.can?(:create_projects, namespace)
@@ -81,8 +94,7 @@ module Projects
       log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
 
       unless @project.gitlab_project_import?
-        @project.create_wiki if @project.wiki_enabled?
-
+        @project.create_wiki unless skip_wiki?
         @project.build_missing_services
 
         @project.create_labels
@@ -96,6 +108,10 @@ module Projects
       end
     end
 
+    def skip_wiki?
+      !@project.feature_available?(:wiki, current_user) || @skip_wiki
+    end
+
     def save_project_and_import_data(import_data)
       Project.transaction do
         @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 8a53f65aec1a82bf4770a1a9d0df637e694113ec..a08c6fcd94b1504cf0be871d804cbd18907ef34c 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -27,6 +27,8 @@ module Projects
       # Git data (e.g. a list of branch names).
       flush_caches(project, wiki_path)
 
+      Projects::UnlinkForkService.new(project, current_user).execute
+
       Project.transaction do
         project.destroy!
 
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index de6dc38cc8e345f87c22445f4622a2a11086f349..a2b23ea61714a3f14424899258686eb8c3611d59 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -8,7 +8,6 @@ module Projects
         name:                   @project.name,
         path:                   @project.path,
         shared_runners_enabled: @project.shared_runners_enabled,
-        builds_enabled:         @project.builds_enabled,
         namespace_id:           @params[:namespace].try(:id) || current_user.namespace.id
       }
 
@@ -17,6 +16,11 @@ module Projects
       end
 
       new_project = CreateService.new(current_user, new_params).execute
+      return new_project unless new_project.persisted?
+
+      builds_access_level = @project.project_feature.builds_access_level
+      new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+
       new_project
     end
 
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 29b3981f49f76f1f3b824f56bd820869cec8c65b..4b8946f8ee21f4f7d8575fbffae0e1e860bd045c 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -7,6 +7,8 @@
 #
 module Projects
   class HousekeepingService < BaseService
+    include Gitlab::CurrentSettings
+
     LEASE_TIMEOUT = 3600
 
     class LeaseTaken < StandardError
@@ -20,42 +22,77 @@ module Projects
     end
 
     def execute
-      raise LeaseTaken unless try_obtain_lease
+      lease_uuid = try_obtain_lease
+      raise LeaseTaken unless lease_uuid.present?
 
-      execute_gitlab_shell_gc
+      execute_gitlab_shell_gc(lease_uuid)
     end
 
     def needed?
-      @project.pushes_since_gc >= 10
+      pushes_since_gc > 0 && period_match? && housekeeping_enabled?
     end
 
     def increment!
-      if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain
-        Gitlab::Metrics.measure(:increment_pushes_since_gc) do
-          update_pushes_since_gc(@project.pushes_since_gc + 1)
-        end
+      Gitlab::Metrics.measure(:increment_pushes_since_gc) do
+        @project.increment_pushes_since_gc
       end
     end
 
     private
 
-    def execute_gitlab_shell_gc
-      GitGarbageCollectWorker.perform_async(@project.id)
+    def execute_gitlab_shell_gc(lease_uuid)
+      GitGarbageCollectWorker.perform_async(@project.id, task, lease_key, lease_uuid)
     ensure
-      Gitlab::Metrics.measure(:reset_pushes_since_gc) do
-        update_pushes_since_gc(0)
+      if pushes_since_gc >= gc_period
+        Gitlab::Metrics.measure(:reset_pushes_since_gc) do
+          @project.reset_pushes_since_gc
+        end
       end
     end
 
-    def update_pushes_since_gc(new_value)
-      @project.update_column(:pushes_since_gc, new_value)
-    end
-
     def try_obtain_lease
       Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
-        lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+        lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
         lease.try_obtain
       end
     end
+
+    def lease_key
+      "project_housekeeping:#{@project.id}"
+    end
+
+    def pushes_since_gc
+      @project.pushes_since_gc
+    end
+
+    def task
+      if pushes_since_gc % gc_period == 0
+        :gc
+      elsif pushes_since_gc % full_repack_period == 0
+        :full_repack
+      else
+        :incremental_repack
+      end
+    end
+
+    def period_match?
+      [gc_period, full_repack_period, repack_period].any? { |period| pushes_since_gc % period == 0 }
+    end
+
+    def housekeeping_enabled?
+      current_application_settings.housekeeping_enabled
+    end
+
+    def gc_period
+      current_application_settings.housekeeping_gc_period
+    end
+
+    def full_repack_period
+      current_application_settings.housekeeping_full_repack_period
+    end
+
+    def repack_period
+      current_application_settings.housekeeping_incremental_repack_period
+    end
   end
 end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cdad0426b02692c0774591cb1416b079cde0693f..d7221fe993c145059191eabe78eb43b1d18b8087 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -29,7 +29,7 @@ module Projects
       if unknown_url?
         # In this case, we only want to import issues, not a repository.
         create_repository
-      else
+      elsif !project.repository_exists?
         import_repository
       end
     end
@@ -44,6 +44,11 @@ module Projects
       begin
         gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
       rescue => e
+        # Expire cache to prevent scenarios such as:
+        # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
+        # 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}"
       end
     end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 02c4eee3d02b2eed9a22ea12a4f81254ce475f8f..d38328403c1641a1a5559b7f37eeb085ff7a4ef4 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,40 +1,28 @@
 module Projects
   class ParticipantsService < BaseService
-    def execute(noteable_type, noteable_id)
-      @noteable_type = noteable_type
-      @noteable_id = noteable_id
+    attr_reader :noteable
+    
+    def execute(noteable)
+      @noteable = noteable
+
       project_members = sorted(project.team.members)
-      participants = target_owner + participants_in_target + all_members + groups + project_members
+      participants = noteable_owner + participants_in_noteable + all_members + groups + project_members
       participants.uniq
     end
 
-    def target
-      @target ||=
-        case @noteable_type
-        when "Issue"
-          project.issues.find_by_iid(@noteable_id)
-        when "MergeRequest"
-          project.merge_requests.find_by_iid(@noteable_id)
-        when "Commit"
-          project.commit(@noteable_id)
-        else
-          nil
-        end
-    end
-
-    def target_owner
-      return [] unless target && target.author.present?
+    def noteable_owner
+      return [] unless noteable && noteable.author.present?
 
       [{
-        name: target.author.name,
-        username: target.author.username
+        name: noteable.author.name,
+        username: noteable.author.username
       }]
     end
 
-    def participants_in_target
-      return [] unless target
+    def participants_in_noteable
+      return [] unless noteable
 
-      users = target.participants(current_user)
+      users = noteable.participants(current_user)
       sorted(users)
     end
 
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index bc7f8bf433b6ed5f77bfdc8310fb0d0fa92c9b50..28470f59807cb48c578d77e354b22aadbfe6aa86 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -28,6 +28,7 @@ module Projects
       Project.transaction do
         old_path = project.path_with_namespace
         old_namespace = project.namespace
+        old_group = project.group
         new_path = File.join(new_namespace.try(:path) || '', project.path)
 
         if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
@@ -57,6 +58,9 @@ module Projects
         # Move wiki repo also if present
         gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
 
+        # Move missing group labels to project
+        Labels::TransferService.new(current_user, old_group, project).execute
+
         # clear project cached events
         project.reset_events_cache
 
diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2040dfa03a86825c90e52d4d0b8867de0c63a76
--- /dev/null
+++ b/app/services/protected_branches/api_create_service.rb
@@ -0,0 +1,29 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+  class ApiCreateService < BaseService
+    def execute
+      push_access_level =
+        if params.delete(:developers_can_push)
+          Gitlab::Access::DEVELOPER
+        else
+          Gitlab::Access::MASTER
+        end
+
+      merge_access_level =
+        if params.delete(:developers_can_merge)
+          Gitlab::Access::DEVELOPER
+        else
+          Gitlab::Access::MASTER
+        end
+
+      @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
+                     merge_access_levels_attributes: [{ access_level: merge_access_level }])
+
+      service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
+      service.execute
+    end
+  end
+end
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..050cb3b738b6cc92993cd4ddafe0607391bff69a
--- /dev/null
+++ b/app/services/protected_branches/api_update_service.rb
@@ -0,0 +1,47 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+  class ApiUpdateService < BaseService
+    def execute(protected_branch)
+      @developers_can_push = params.delete(:developers_can_push)
+      @developers_can_merge = params.delete(:developers_can_merge)
+
+      @protected_branch = protected_branch
+
+      protected_branch.transaction do
+        delete_redundant_access_levels
+
+        case @developers_can_push
+        when true
+          params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+        when false
+          params.merge!(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 }])
+        when false
+          params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+        end
+
+        service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
+        service.execute(protected_branch)
+      end
+    end
+
+    private
+
+    def delete_redundant_access_levels
+      unless @developers_can_merge.nil?
+        @protected_branch.merge_access_levels.destroy_all
+      end
+
+      unless @developers_can_push.nil?
+        @protected_branch.push_access_levels.destroy_all
+      end
+    end
+  end
+end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a81194a5f49d389c259fd3bf1bf4df794c1b969
--- /dev/null
+++ b/app/services/slash_commands/interpret_service.rb
@@ -0,0 +1,265 @@
+module SlashCommands
+  class InterpretService < BaseService
+    include Gitlab::SlashCommands::Dsl
+
+    attr_reader :issuable
+
+    # Takes a text and interprets the commands that are extracted from it.
+    # Returns the content without commands, and hash of changes to be applied to a record.
+    def execute(content, issuable)
+      @issuable = issuable
+      @updates = {}
+
+      opts = {
+        issuable:     issuable,
+        current_user: current_user,
+        project:      project
+      }
+
+      content, commands = extractor.extract_commands(content, opts)
+
+      commands.each do |name, arg|
+        definition = self.class.command_definitions_by_name[name.to_sym]
+        next unless definition
+
+        definition.execute(self, opts, arg)
+      end
+
+      [content, @updates]
+    end
+
+    private
+
+    def extractor
+      Gitlab::SlashCommands::Extractor.new(self.class.command_definitions)
+    end
+
+    desc do
+      "Close this #{issuable.to_ability_name.humanize(capitalize: false)}"
+    end
+    condition do
+      issuable.persisted? &&
+        issuable.open? &&
+        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+    end
+    command :close do
+      @updates[:state_event] = 'close'
+    end
+
+    desc do
+      "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}"
+    end
+    condition do
+      issuable.persisted? &&
+        issuable.closed? &&
+        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+    end
+    command :reopen do
+      @updates[:state_event] = 'reopen'
+    end
+
+    desc 'Change title'
+    params '<New title>'
+    condition do
+      issuable.persisted? &&
+        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+    end
+    command :title do |title_param|
+      @updates[:title] = title_param
+    end
+
+    desc 'Assign'
+    params '@user'
+    condition do
+      current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :assign do |assignee_param|
+      user = extract_references(assignee_param, :user).first
+      user ||= User.find_by(username: assignee_param)
+
+      @updates[:assignee_id] = user.id if user
+    end
+
+    desc 'Remove assignee'
+    condition do
+      issuable.persisted? &&
+        issuable.assignee_id? &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :unassign do
+      @updates[:assignee_id] = nil
+    end
+
+    desc 'Set milestone'
+    params '%"milestone"'
+    condition do
+      current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+        project.milestones.active.any?
+    end
+    command :milestone do |milestone_param|
+      milestone = extract_references(milestone_param, :milestone).first
+      milestone ||= project.milestones.find_by(title: milestone_param.strip)
+
+      @updates[:milestone_id] = milestone.id if milestone
+    end
+
+    desc 'Remove milestone'
+    condition do
+      issuable.persisted? &&
+        issuable.milestone_id? &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :remove_milestone do
+      @updates[:milestone_id] = nil
+    end
+
+    desc 'Add label(s)'
+    params '~label1 ~"label 2"'
+    condition do
+      available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+
+      current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+        available_labels.any?
+    end
+    command :label do |labels_param|
+      label_ids = find_label_ids(labels_param)
+
+      if label_ids.any?
+        @updates[:add_label_ids] ||= []
+        @updates[:add_label_ids] += label_ids
+
+        @updates[:add_label_ids].uniq!
+      end
+    end
+
+    desc 'Remove all or specific label(s)'
+    params '~label1 ~"label 2"'
+    condition do
+      issuable.persisted? &&
+        issuable.labels.any? &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :unlabel do |labels_param = nil|
+      if labels_param.present?
+        label_ids = find_label_ids(labels_param)
+
+        if label_ids.any?
+          @updates[:remove_label_ids] ||= []
+          @updates[:remove_label_ids] += label_ids
+
+          @updates[:remove_label_ids].uniq!
+        end
+      else
+        @updates[:label_ids] = []
+      end
+    end
+
+    desc 'Replace all label(s)'
+    params '~label1 ~"label 2"'
+    condition do
+      issuable.persisted? &&
+        issuable.labels.any? &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :relabel do |labels_param|
+      label_ids = find_label_ids(labels_param)
+
+      if label_ids.any?
+        @updates[:label_ids] ||= []
+        @updates[:label_ids] += label_ids
+
+        @updates[:label_ids].uniq!
+      end
+    end
+
+    desc 'Add a todo'
+    condition do
+      issuable.persisted? &&
+        !TodoService.new.todo_exist?(issuable, current_user)
+    end
+    command :todo do
+      @updates[:todo_event] = 'add'
+    end
+
+    desc 'Mark todo as done'
+    condition do
+      issuable.persisted? &&
+        TodoService.new.todo_exist?(issuable, current_user)
+    end
+    command :done do
+      @updates[:todo_event] = 'done'
+    end
+
+    desc 'Subscribe'
+    condition do
+      issuable.persisted? &&
+        !issuable.subscribed?(current_user)
+    end
+    command :subscribe do
+      @updates[:subscription_event] = 'subscribe'
+    end
+
+    desc 'Unsubscribe'
+    condition do
+      issuable.persisted? &&
+        issuable.subscribed?(current_user)
+    end
+    command :unsubscribe do
+      @updates[:subscription_event] = 'unsubscribe'
+    end
+
+    desc 'Set due date'
+    params '<in 2 days | this Friday | December 31st>'
+    condition do
+      issuable.respond_to?(:due_date) &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :due do |due_date_param|
+      due_date = Chronic.parse(due_date_param).try(:to_date)
+
+      @updates[:due_date] = due_date if due_date
+    end
+
+    desc 'Remove due date'
+    condition do
+      issuable.persisted? &&
+        issuable.respond_to?(:due_date) &&
+        issuable.due_date? &&
+        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+    end
+    command :remove_due_date do
+      @updates[:due_date] = nil
+    end
+
+    desc do
+      "Toggle the Work In Progress status"
+    end
+    condition do
+      issuable.persisted? &&
+        issuable.respond_to?(:work_in_progress?) &&
+        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+    end
+    command :wip do
+      @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
+    end
+
+    # This is a dummy command, so that it appears in the autocomplete commands
+    desc 'CC'
+    params '@user'
+    command :cc
+
+    def find_label_ids(labels_param)
+      label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
+      labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
+
+      label_ids_by_reference | labels_ids_by_name
+    end
+
+    def extract_references(arg, type)
+      ext = Gitlab::ReferenceExtractor.new(project, current_user)
+      ext.analyze(arg, author: current_user)
+
+      ext.references(type)
+    end
+  end
+end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 1fb72cf89e9ee794878430163c468593d1b286a8..a2bfa422c9d4572f2becdd5211119518fd7588d4 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -72,7 +72,7 @@ class SystemHooksService
       return 'user_add_to_group'      if event == :create
       return 'user_remove_from_group' if event == :destroy
     else
-      "#{model.class.name.downcase}_#{event.to_s}"
+      "#{model.class.name.downcase}_#{event}"
     end
   end
 
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e13dc9265b83359d122f99aa06357284ff7bbca4..1ce66d50368805498468337c29c2831463b7de8d 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -24,6 +24,7 @@ module SystemNoteService
     body = "Added #{commits_text}:\n\n"
     body << existing_commit_summary(noteable, existing_commits, oldrev)
     body << new_commit_summary(new_commits).join("\n")
+    body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
 
     create_note(noteable: noteable, project: project, author: author, note: body)
   end
@@ -158,6 +159,12 @@ module SystemNoteService
     create_note(noteable: noteable, project: project, author: author, note: body)
   end
 
+  def self.resolve_all_discussions(merge_request, project, author)
+    body = "Resolved all discussions"
+
+    create_note(noteable: merge_request, project: project, author: author, note: body)
+  end
+
   # Called when the title of a Noteable is changed
   #
   # noteable  - Noteable object that responds to `title`
@@ -239,7 +246,7 @@ module SystemNoteService
         'deleted'
       end
 
-    body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
+    body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
     create_note(noteable: noteable, project: project, author: author, note: body)
   end
 
@@ -248,8 +255,7 @@ module SystemNoteService
   #
   #   "Started branch `201-issue-branch-button`"
   def new_issue_branch(issue, project, author, branch)
-    h = Gitlab::Routing.url_helpers
-    link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+    link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
 
     body = "Started branch [`#{branch}`](#{link})"
     create_note(noteable: issue, project: project, author: author, note: body)
@@ -263,11 +269,11 @@ module SystemNoteService
   #
   # Example Note text:
   #
-  #   "mentioned in #1"
+  #   "Mentioned in #1"
   #
-  #   "mentioned in !2"
+  #   "Mentioned in !2"
   #
-  #   "mentioned in 54f7727c"
+  #   "Mentioned in 54f7727c"
   #
   # See cross_reference_note_content.
   #
@@ -302,7 +308,7 @@ module SystemNoteService
 
   # Check if a cross-reference is disallowed
   #
-  # This method prevents adding a "mentioned in !1" note on every single commit
+  # This method prevents adding a "Mentioned in !1" note on every single commit
   # in a merge request. Additionally, it prevents the creation of references to
   # external issues (which would fail).
   #
@@ -341,7 +347,7 @@ module SystemNoteService
       notes = notes.where(noteable_id: noteable.id)
     end
 
-    notes_for_mentioner(mentioner, noteable, notes).count > 0
+    notes_for_mentioner(mentioner, noteable, notes).exists?
   end
 
   # Build an Array of lines detailing each commit added in a merge request
@@ -411,7 +417,7 @@ module SystemNoteService
   end
 
   def cross_reference_note_prefix
-    'mentioned in '
+    'Mentioned in '
   end
 
   def cross_reference_note_content(gfm_reference)
@@ -460,4 +466,20 @@ module SystemNoteService
   def escape_html(text)
     Rack::Utils.escape_html(text)
   end
+
+  def url_helpers
+    @url_helpers ||= Gitlab::Routing.url_helpers
+  end
+
+  def diff_comparison_url(merge_request, project, oldrev)
+    diff_id = merge_request.merge_request_diff.id
+
+    url_helpers.diffs_namespace_project_merge_request_url(
+      project.namespace,
+      project,
+      merge_request.iid,
+      diff_id: diff_id,
+      start_sha: oldrev
+    )
+  end
 end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index eb833dd82ac1c69cc7f57128c92b084f4bc8a32a..f8e6b2ef0942300c5014c6dc1cd95738aa100f85 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,6 +31,14 @@ class TodoService
     mark_pending_todos_as_done(issue, current_user)
   end
 
+  # When we destroy an issue we should:
+  #
+  #  * refresh the todos count cache for the current user
+  #
+  def destroy_issue(issue, current_user)
+    destroy_issuable(issue, current_user)
+  end
+
   # When we reassign an issue we should:
   #
   #  * create a pending todo for new assignee if issue is assigned
@@ -64,6 +72,14 @@ class TodoService
     mark_pending_todos_as_done(merge_request, current_user)
   end
 
+  # When we destroy a merge request we should:
+  #
+  #  * refresh the todos count cache for the current user
+  #
+  def destroy_merge_request(merge_request, current_user)
+    destroy_issuable(merge_request, current_user)
+  end
+
   # When we reassign a merge request we should:
   #
   #  * creates a pending todo for new assignee if merge request is assigned
@@ -142,9 +158,14 @@ class TodoService
 
   # When user marks some todos as done
   def mark_todos_as_done(todos, current_user)
-    todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
+    mark_todos_as_done_by_ids(todos.select(&:id), current_user)
+  end
+
+  def mark_todos_as_done_by_ids(ids, current_user)
+    todos = current_user.todos.where(id: ids)
 
-    marked_todos = todos.update_all(state: :done)
+    # Only return those that are not really on that state
+    marked_todos = todos.where.not(state: :done).update_all(state: :done)
     current_user.update_todos_count_cache
     marked_todos
   end
@@ -155,6 +176,10 @@ class TodoService
     create_todos(current_user, attributes)
   end
 
+  def todo_exist?(issuable, current_user)
+    TodosFinder.new(current_user).execute.exists?(target: issuable)
+  end
+
   private
 
   def create_todos(users, attributes)
@@ -178,6 +203,10 @@ class TodoService
     create_mention_todos(issuable.project, issuable, author)
   end
 
+  def destroy_issuable(issuable, user)
+    user.update_todos_count_cache
+  end
+
   def toggling_tasks?(issuable)
     issuable.previous_changes.include?('description') &&
       issuable.tasks? && issuable.updated_tasks.any?
@@ -244,12 +273,12 @@ class TodoService
   end
 
   def reject_users_without_access(users, project, target)
-    if target.is_a?(Note) && target.for_issue?
+    if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
       target = target.noteable
     end
 
-    if target.is_a?(Issue)
-      select_users(users, :read_issue, target)
+    if target.is_a?(Issuable)
+      select_users(users, :"read_#{target.to_ability_name}", target)
     else
       select_users(users, :read_project, project)
     end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 7a35958cc5f494eb2fd6db9ade91549e046d5f51..2821ecf0a888de67b93e6f6092d326708d18af12 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -5,7 +5,8 @@
 # Values are checked for formatting and exclusion from a list of reserved path
 # names.
 class NamespaceValidator < ActiveModel::EachValidator
-  RESERVED = %w(
+  RESERVED = %w[
+    .well-known
     admin
     all
     assets
@@ -23,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator
     projects
     public
     repository
+    robots.txt
     s
     search
     services
@@ -31,7 +33,7 @@ class NamespaceValidator < ActiveModel::EachValidator
     u
     unsubscribes
     users
-  ).freeze
+  ].freeze
 
   def validate_each(record, attribute, value)
     unless value =~ Gitlab::Regex.namespace_regex
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index dd2e7ebd0309acde521152646fa11fba078bff1b..05f3d9a3b50eadb61360158db81e9449dcbee9fd 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,6 +1,8 @@
 - reporter = abuse_report.reporter
 - user = abuse_report.user
 %tr
+  %th.visible-xs-block.visible-sm-block
+    %strong User
   %td
     - if user
       = link_to user.name, user
@@ -9,6 +11,7 @@
     - else
       (removed)
   %td
+    %strong.subheading.visible-xs-block.visible-sm-block Reported by
     - if reporter
       = link_to reporter.name, reporter
     - else
@@ -16,16 +19,16 @@
     .light.small
       = time_ago_with_tooltip(abuse_report.created_at)
   %td
-    = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+    %strong.subheading.visible-xs-block.visible-sm-block Message
+    .message
+      = markdown_field(abuse_report, :message)
   %td
     - if user
       = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
-        data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
-
-  %td
+        data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
     - if user && !user.blocked?
-      = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+      = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
     - else
-      .btn.btn-xs.disabled
+      .btn.btn-sm.disabled.btn-block
         Already Blocked
-    = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
+    = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index bc4a9cedb2caebe5c570ebff6067d6be1003643d..7bbc75db9ff3fefd61af4de52acf2b428f85fe76 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,17 +1,20 @@
-- page_title "Abuse Reports"
+- page_title 'Abuse Reports'
 %h3.page-title Abuse Reports
 %hr
-- if @abuse_reports.present?
-  .table-holder
-    %table.table
-      %thead
-        %tr
-          %th User
-          %th Reported by
-          %th Message
-          %th Primary action
-          %th
-      = render @abuse_reports
-  = paginate @abuse_reports
-- else
-  %h4 There are no abuse reports
+.abuse-reports
+  - if @abuse_reports.present?
+    .table-holder
+      %table.table
+        %thead.hidden-sm.hidden-xs
+          %tr
+            %th User
+            %th Reported by
+            %th.wide Message
+            %th Action
+        = render @abuse_reports
+  - else
+    .no-reports
+      %span.pull-left
+        There are no abuse reports!
+      .pull-left
+        = emoji_icon 'tada'
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 92e2dae48420edae270b92e52166b3bc05261250..9175b3d3f964dd93d0cb8491ff02372c4b239709 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -13,7 +13,7 @@
     .col-sm-10
       = f.text_area :description, class: "form-control", rows: 10
       .hint
-        Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}.
+        Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
   .form-group
     = f.label :logo, class: 'control-label'
     .col-sm-10
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index 6c51639b8405f757ec18105023bd40d0b3e341f9..1af7dd5bb67f8f63488ada530f7a2e8f2efd127c 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,9 +1,12 @@
-- page_title "Preview | Appearance"
+= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
 .login-box
-  .login-heading
-    %h3 Existing user? Sign in
-  %form
-    = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
-    = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
-    = button_tag "Sign in", class: "btn-create btn"
+  %form.gl-show-field-errors
+    .form-group
+      = label_tag :login
+      = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
+    .form-group
+      = label_tag :password
+      = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
+    .form-group
+      = button_tag "Sign in", class: "btn-create btn"
 
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index c7fd344eea2d4712341880de1d576db4428a1adb..450ec322f2c3adec07efbbf71c9b3810d5a82e05 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -49,28 +49,6 @@
         = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
         %span.help-block#clone-protocol-help
           Allow only the selected protocols to be used for Git access.
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :version_check_enabled do
-            = f.check_box :version_check_enabled
-            Version check enabled
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :email_author_in_body do
-            = f.check_box :email_author_in_body
-            Include author name in notification email body
-          .help-block
-            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.
-    .form-group
-      = f.label :admin_notification_email, class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :admin_notification_email, class: 'form-control'
-        .help-block
-          Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
 
   %fieldset
     %legend Account and Limit Settings
@@ -243,7 +221,11 @@
   %fieldset
     %legend Metrics
     %p
-      These settings require a restart to take effect.
+      Setup InfluxDB to measure a wide variety of statistics like the time spent
+      in running SQL queries. These settings require a
+      = link_to 'restart', help_page_path('administration/restart_gitlab')
+      to take effect.
+      = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
     .form-group
       .col-sm-offset-2.col-sm-10
         .checkbox
@@ -340,6 +322,15 @@
           Generate API key at
           %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
 
+  %fieldset
+    %legend Abuse reports
+    .form-group
+      = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :admin_notification_email, class: 'form-control'
+        .help-block
+          Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
   %fieldset
     %legend Error Reporting and Logging
     %p
@@ -362,9 +353,9 @@
   %fieldset
     %legend Repository Storage
     .form-group
-      = f.label :repository_storage, 'Storage path for new projects', class: 'control-label col-sm-2'
+      = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
       .col-sm-10
-        = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control'
+        = f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
         .help-block
           Manage repository storage paths. Learn more in the
           = succeed "." do
@@ -388,6 +379,87 @@
         .help-block
           If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
 
+  %fieldset
+    %legend Koding
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :koding_enabled do
+            = f.check_box :koding_enabled
+            Enable Koding
+    .form-group
+      = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+        .help-block
+          Koding has integration enabled out of the box for the
+          %strong gitlab
+          team, and you need to provide that team's URL here. Learn more in the
+          = succeed "." do
+            = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
+  %fieldset
+    %legend Usage statistics
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :version_check_enabled do
+            = f.check_box :version_check_enabled
+            Version check enabled
+          .help-block
+            Let GitLab inform you when an update is available.
+
+  %fieldset
+    %legend Email
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :email_author_in_body do
+            = f.check_box :email_author_in_body
+            Include author name in notification email body
+          .help-block
+            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.
+
+  %fieldset
+    %legend Automatic Git repository housekeeping
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :housekeeping_enabled do
+            = f.check_box :housekeeping_enabled
+            Enable automatic repository housekeeping (git repack, git gc)
+          .help-block
+            If you keep automatic housekeeping disabled for a long time Git
+            repository access on your GitLab server will become slower and your
+            repositories will use more disk space. We recommend to always leave
+            this enabled.
+        .checkbox
+          = f.label :housekeeping_bitmaps_enabled do
+            = f.check_box :housekeeping_bitmaps_enabled
+            Enable Git pack file bitmap creation
+          .help-block
+            Creating pack file bitmaps makes housekeeping take a little longer but
+            bitmaps should accelerate 'git clone' performance.
+    .form-group
+      = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+        .help-block
+          Number of Git pushes after which an incremental 'git repack' is run.
+    .form-group
+      = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+        .help-block
+          Number of Git pushes after which a full 'git repack' is run.
+    .form-group
+      = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :housekeeping_gc_period, class: 'form-control'
+        .help-block
+          Number of Git pushes after which 'git gc' is run.
 
   .form-actions
     = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 89d7a40d6b0210c3e27bcf330c248c1c41680417..b3530915068f18ea7c65cd744910d1123e0fad77 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,22 +1,25 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
-    = nav_link(controller: :system_info) do
-      = link_to admin_system_info_path, title: 'System Info' do
-        %span
-          System Info
-    = nav_link(controller: :background_jobs) do
-      = link_to admin_background_jobs_path, title: 'Background Jobs' do
-        %span
-          Background Jobs
-    = nav_link(controller: :logs) do
-      = link_to admin_logs_path, title: 'Logs' do
-        %span
-          Logs
-    = nav_link(controller: :health_check) do
-      = link_to admin_health_check_path, title: 'Health Check' do
-        %span
-          Health Check
-    = nav_link(controller: :requests_profiles) do
-      = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
-        %span
-          Requests Profiles
+= 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(controller: :system_info) do
+          = link_to admin_system_info_path, title: 'System Info' do
+            %span
+              System Info
+        = nav_link(controller: :background_jobs) do
+          = link_to admin_background_jobs_path, title: 'Background Jobs' do
+            %span
+              Background Jobs
+        = nav_link(controller: :logs) do
+          = link_to admin_logs_path, title: 'Logs' do
+            %span
+              Logs
+        = nav_link(controller: :health_check) do
+          = link_to admin_health_check_path, title: 'Health Check' do
+            %span
+              Health Check
+        = nav_link(controller: :requests_profiles) do
+          = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+            %span
+              Requests Profiles
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 4f680b507c493983d66295c120aebebd6610f200..05855db963a44004d8fe5cc0e855e15ad772d5b6 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -28,14 +28,10 @@
               %th COMMAND
             %tbody
               - @sidekiq_processes.each do |process|
-                - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
-                - data = process.strip.split(' ')
                 %tr
                   %td= gitlab_config.user
-                  - 5.times do
-                    %td= data.shift
-                  %td= data.join(' ')
-
+                  - parse_sidekiq_ps(process).each do |value|
+                    %td= value
         .clearfix
           %p
             %i.fa.fa-exclamation-circle
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 6b157abf8422db584d0fdcc12dd79ccb6411c677..3132d157f29bb8697702fd878471bdcc2b84b5b1 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -1,7 +1,10 @@
 .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
   = icon('bullhorn')
   .js-broadcast-message-preview
-    = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
+    - if @broadcast_message.message.present?
+      = render_broadcast_message(@broadcast_message)
+    - else
+      = "Your message here"
 
 = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
   = form_errors(@broadcast_message)
@@ -18,11 +21,11 @@
   .form-group.js-toggle-colors-container.hide
     = f.label :color, "Background Color", class: 'control-label'
     .col-sm-10
-      = f.color_field :color, class: "form-control"
+      = f.text_field :color, class: "form-control"
   .form-group.js-toggle-colors-container.hide
     = f.label :font, "Font Color", class: 'control-label'
     .col-sm-10
-      = f.color_field :font, class: "form-control"
+      = f.text_field :font, class: "form-control"
   .form-group
     = f.label :starts_at, class: 'control-label'
     .col-sm-10.datetime-controls
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
index fbc9453c72ee5b5e5bec4fefd71dc4af1d3bf5c3..c72e59640d7f52b3796deded7706fc560f330e84 100644
--- a/app/views/admin/broadcast_messages/preview.js.haml
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -1 +1 @@
-$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
deleted file mode 100644
index 352adbedee4e0a88401c8aed59bac7751537f215..0000000000000000000000000000000000000000
--- a/app/views/admin/builds/_build.html.haml
+++ /dev/null
@@ -1,77 +0,0 @@
-- project = build.project
-%tr.build.commit
-  %td.status
-    = ci_status_with_icon(build.status)
-
-  %td
-    .branch-commit
-      - if can?(current_user, :read_build, build.project)
-        = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
-          %span.build-link ##{build.id}
-      - else
-        %span.build-link ##{build.id}
-
-      - if build.ref
-        .icon-container
-          = build.tag? ? icon('tag') : icon('code-fork')
-        = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
-      - else
-        .light none
-      .icon-container
-        = custom_icon("icon_commit")
-
-      = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id"
-      - if build.stuck?
-        %i.fa.fa-warning.text-warning
-
-      .label-container
-        - if build.tags.any?
-          - build.tags.each do |tag|
-            %span.label.label-primary
-              = tag
-        - if build.try(:trigger_request)
-          %span.label.label-info triggered
-        - if build.try(:allow_failure)
-          %span.label.label-danger allowed to fail
-
-  %td
-    - if project
-      = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
-
-  %td
-    - if build.try(:runner)
-      = runner_link(build.runner)
-    - else
-      .light none
-
-  %td
-    #{build.stage} / #{build.name}
-
-  %td
-    - if build.duration
-      %p.duration
-        = custom_icon("icon_timer")
-        = duration_in_numbers(build.finished_at, build.started_at)
-
-    - if build.finished_at
-      %p.finished-at
-        = icon("calendar")
-        %span #{time_ago_with_tooltip(build.finished_at)}
-
-  - if defined?(coverage) && coverage
-    %td.coverage
-      - if build.try(:coverage)
-        #{build.coverage}%
-
-  %td
-    .pull-right
-      - if can?(current_user, :read_build, project) && build.artifacts?
-        = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
-          %i.fa.fa-download
-      - if can?(current_user, :update_build, build.project)
-        - if build.active?
-          = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
-            %i.fa.fa-remove.cred
-        - elsif defined?(allow_retry) && allow_retry && build.retryable?
-          = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
-            %i.fa.fa-refresh
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 3d77634d8fafe6b9ce72261c28845377a5599795..26a8846b609f69cbaeac928268af82a82ca5d2f6 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -4,26 +4,8 @@
 %div{ class: container_class }
 
   .top-area
-    %ul.nav-links
-      %li{class: ('active' if @scope.nil?)}
-        = link_to admin_builds_path do
-          All
-          %span.badge.js-totalbuilds-count= @all_builds.count(:id)
-
-      %li{class: ('active' if @scope == 'pending')}
-        = link_to admin_builds_path(scope: :pending) do
-          Pending
-          %span.badge= number_with_delimiter(@all_builds.pending.count(:id))
-
-      %li{class: ('active' if @scope == 'running')}
-        = link_to admin_builds_path(scope: :running) do
-          Running
-          %span.badge= number_with_delimiter(@all_builds.running.count(:id))
-
-      %li{class: ('active' if @scope == 'finished')}
-        = link_to admin_builds_path(scope: :finished) do
-          Finished
-          %span.badge= number_with_delimiter(@all_builds.finished.count(:id))
+    - build_path_proc = ->(scope) { admin_builds_path(scope: scope) }
+    = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
 
     .nav-controls
       - if @all_builds.running_or_pending.any?
@@ -33,23 +15,4 @@
     #{(@scope || 'all').capitalize} builds
 
   %ul.content-list.builds-content-list
-    - if @builds.blank?
-      %li
-        .nothing-here-block No builds to show
-    - else
-      .table-holder
-        %table.table.builds
-          %thead
-            %tr
-              %th Status
-              %th Commit
-              %th Project
-              %th Runner
-              %th Name
-              %th
-              %th
-
-          - @builds.each do |build|
-            = render "admin/builds/build", build: build
-
-      = paginate @builds, theme: 'gitlab'
+    = render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b74da64f82eb04173398663b0b785a9727271911..ec40391a3e3a83d2afce1940fdbe65e0672bd840 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,26 +1,29 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
-    = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
-      = link_to admin_root_path, title: 'Overview' do
-        %span
-          Overview
-    = nav_link(controller: [:admin, :projects]) do
-      = link_to admin_namespaces_projects_path, title: 'Projects' do
-        %span
-          Projects
-    = nav_link(controller: :users) do
-      = link_to admin_users_path, title: 'Users' do
-        %span
-          Users
-    = nav_link(controller: :groups) do
-      = link_to admin_groups_path, title: 'Groups' do
-        %span
-          Groups
-    = nav_link path: 'builds#index' do
-      = link_to admin_builds_path, title: 'Builds' do
-        %span
-          Builds
-    = nav_link path: ['runners#index', 'runners#show'] do
-      = link_to admin_runners_path, title: 'Runners' do
-        %span
-          Runners
+= 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(controller: :dashboard, html_options: {class: 'home'}) do
+          = link_to admin_root_path, title: 'Overview' do
+            %span
+              Overview
+        = nav_link(controller: [:admin, :projects]) do
+          = link_to admin_namespaces_projects_path, title: 'Projects' do
+            %span
+              Projects
+        = nav_link(controller: :users) do
+          = link_to admin_users_path, title: 'Users' do
+            %span
+              Users
+        = nav_link(controller: :groups) do
+          = link_to admin_groups_path, title: 'Groups' do
+            %span
+              Groups
+        = nav_link path: 'builds#index' do
+          = link_to admin_builds_path, title: 'Builds' do
+            %span
+              Builds
+        = nav_link path: ['runners#index', 'runners#show'] do
+          = link_to admin_runners_path, title: 'Runners' do
+            %span
+              Runners
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e6687f43816d81d36d3ef78b9425de668b4a17e4..1db2150f3366f15e9c8acd63ce7d2f54078ec163 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -63,6 +63,11 @@
           Reply by email
           %span.light.pull-right
             = boolean_to_icon Gitlab::IncomingEmail.enabled?
+        %p
+          Container Registry
+          %span.light.pull-right
+            = boolean_to_icon Gitlab.config.registry.enabled
+
       .col-md-4
         %h4
           Components
@@ -82,7 +87,7 @@
         %p
           GitLab Workhorse
           %span.pull-right
-            = Gitlab::Workhorse.version
+            = gitlab_workhorse_version
         %p
           GitLab API
           %span.pull-right
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5f7fdfdb011801cf71ded2c5fd22ade8f7c4717b..817910f7ddf0cc44d3398731301bcfe466bda153 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -13,6 +13,8 @@
     .col-sm-offset-2.col-sm-10
       = render 'shared/allow_request_access', form: f
 
+  = render 'groups/group_lfs_settings', f: f
+
   - if @group.new_record?
     .form-group
       .col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 77a11e49e20088e4a73a2fc2024fa0c84cf05ac1..664bb417c6a70ea4e76829fc7bc106d2b7193171 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -16,11 +16,12 @@
     %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
       = visibility_level_icon(group.visibility_level, fw: false)
 
-  = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+  .avatar-container.s40
+    = image_tag group_icon(group), class: "avatar s40 hidden-xs"
   .title
     = link_to [:admin, group], class: 'group-name' do
       = group.name
 
   - if group.description.present?
     .description
-      = markdown(group.description, pipeline: :description)
+      = markdown_field(group, :description)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index bb37469440076243a655866968de2f41a1a81c01..40871e32913bc5b6aae16147e98339aa6f8273e4 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -13,7 +13,8 @@
         Group info:
       %ul.well-list
         %li
-          = image_tag group_icon(@group), class: "avatar s60"
+          .avatar-container.s60
+            = image_tag group_icon(@group), class: "avatar s60"
         %li
           %span.light Name:
           %strong= @group.name
@@ -37,6 +38,12 @@
           %strong
             = @group.created_at.to_s(:medium)
 
+        %li
+          %span.light Group Git LFS status:
+          %strong
+            = group_lfs_status(@group)
+            = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+
     .panel.panel-default
       .panel-heading
         %h3.panel-title
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 602cfa9b6fc8e7529acf9d9d8f4415956d8891f1..d5e6bede36a1a72777ba7c1873a00fefa4b56347 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -14,7 +14,7 @@
     .col-sm-10
       .input-group
         .input-group-addon.label-color-preview &nbsp;
-        = f.color_field :color, class: "form-control"
+        = f.text_field :color, class: "form-control"
       .help-block
         Choose any color.
         %br
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index f417b2e44a411087ede12b3cbec7fdd9ec1698e6..be224d6685529df268d7932e1f76b42f2c756d3c 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,7 +1,7 @@
 %li{id: dom_id(label)}
   .label-row
     = render_colored_label(label, tooltip: false)
-    = markdown(label.description, pipeline: :single_line)
+    = markdown_field(label, :description)
     .pull-right
       = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
       = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 676812121d773d2901c1b7ca0e3237975a53bbaf..824edd171f3672d3192f5a6b0eb67ffe625b9ef9 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,7 +1,7 @@
 - @no_container = true
 - page_title "Logs"
 - loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
-             Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
+             Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger,
              Gitlab::RepositoryCheckLogger]
 = render 'admin/background_jobs/head'
 
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 1e755785d907f46ba6871ac399ac24250fd994ee..b37b8d4fee78728d820a7fb0cfc144cbb2016556 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -76,7 +76,8 @@
             .title
               = link_to [:admin, project.namespace.becomes(Namespace), project] do
                 .dash-project-avatar
-                  = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+                  .avatar-container.s40
+                    = project_icon(project, alt: '', class: 'avatar project-avatar s40')
                 %span.project-full-name
                   %span.namespace-name
                     - if project.namespace
@@ -87,7 +88,7 @@
 
             - if project.description.present?
               .description
-                = markdown(project.description, pipeline: :description)
+                = markdown_field(project, :description)
 
       = paginate @projects, theme: 'gitlab'
     - else
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index b2c607361b3d417e9b9755efc11893abcda1a69d..6c7c3c48604aa7a26baf05d8d9cc0d079ee2bacf 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -73,6 +73,12 @@
             %span.light last commit:
             %strong
               = last_commit(@project)
+
+          %li
+            %span.light Git LFS status:
+            %strong
+              = project_lfs_status(@project)
+              = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
         - else
           %li
             %span.light repository:
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a53876d67573d3e767473aacb1635be1292d304a..37bb6a3b0e088eb7caeb1d2291a72abae93cd457 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -5,8 +5,10 @@
 
   %p.prepend-top-default
     %span
-      To register a new runner you should enter the following registration token.
-      With this token the runner will request a unique runner token and use that for future communication.
+      To register a new Runner you should enter the following registration
+      token.
+      With this token the Runner will request a unique Runner token and use
+      that for future communication.
       %br
       Registration token is
       %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
@@ -24,27 +26,27 @@
 
   .bs-callout
     %p
-      A 'runner' is a process which runs a build.
-      You can setup as many runners as you need.
+      A 'Runner' is a process which runs a build.
+      You can setup as many Runners as you need.
       %br
-      Runners can be placed on separate users, servers, and even on your local machine.
+      Runners can be placed on separate users, servers, even on your local machine.
       %br
 
     %div
-      %span Each runner can be in one of the following states:
+      %span Each Runner can be in one of the following states:
       %ul
         %li
           %span.label.label-success shared
-          \- run builds from all unassigned projects
+          \- Runner runs builds from all unassigned projects
         %li
           %span.label.label-info specific
-          \- run builds from assigned projects
+          \- Runner runs builds from assigned projects
         %li
           %span.label.label-warning locked
-          \- runner cannot be assigned to other projects
+          \- Runner cannot be assigned to other projects
         %li
           %span.label.label-danger paused
-          \- runner will not receive any new builds
+          \- Runner will not receive any new builds
 
   .append-bottom-20.clearfix
     .pull-left
@@ -73,4 +75,4 @@
 
       - @runners.each do |runner|
         = render "admin/runners/runner", runner: runner
-  = paginate @runners
+  = paginate @runners, theme: "gitlab"
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 61abfc6ecbe8aa08ff1ccdac37f2b26539094d55..73038164056f7be693c666305358da2b631f3f8b 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,14 +11,14 @@
 
 - if @runner.shared?
   .bs-callout.bs-callout-success
-    %h4 This runner will process builds from ALL UNASSIGNED projects
+    %h4 This Runner will process builds from ALL UNASSIGNED projects
     %p
-      If you want runners to build only specific projects, enable them in the table below.
+      If you want Runners to build only specific projects, enable them in the table below.
       Keep in mind that this is a one way transition.
 - else
   .bs-callout.bs-callout-info
-    %h4 This runner will process builds only from ASSIGNED projects
-    %p You can't make this a shared runner.
+    %h4 This Runner will process builds only from ASSIGNED projects
+    %p You can't make this a shared Runner.
 %hr
 
 .append-bottom-20
@@ -26,7 +26,7 @@
 
 .row
   .col-md-6
-    %h4 Restrict projects for this runner
+    %h4 Restrict projects for this Runner
     - if @runner.projects.any?
       %table.table.assigned-projects
         %thead
@@ -67,11 +67,11 @@
               = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
                 = f.hidden_field :runner_id, value: @runner.id
                 = f.submit 'Enable', class: 'btn btn-xs'
-    = paginate @projects
+    = paginate @projects, theme: "gitlab"
 
   .col-md-6
-    %h4 Recent builds served by this runner
-    %table.table.builds.runner-builds
+    %h4 Recent builds served by this Runner
+    %table.table.ci-table.runner-builds
       %thead
         %tr
           %th Build
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6956e5ab7958a003ae5eead0e8789b5a4daf0e20..bfc6142067a53bf82b91a5256e782ec999997a95 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -9,12 +9,20 @@
       .light-well
         %h4 CPU
         .data
-          %h1= "#{@cpus} cores"
+          - if @cpus
+            %h1= "#{@cpus.length} cores"
+          - else
+            = icon('warning', class: 'text-warning')
+            Unable to collect CPU info
     .col-sm-4
       .light-well
         %h4 Memory
         .data
-          %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}"
+          - if @memory
+            %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
+          - else
+            = icon('warning', class: 'text-warning')
+            Unable to collect memory info
     .col-sm-4
       .light-well
         %h4 Disks
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 357123c2c134407422c612aa9636c32745feeaa6..d3038ae644f8210b19933d4c99efa207d0a03c7b 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -10,7 +10,7 @@
           = hidden_field_tag "filter", h(params[:filter])
         .search-holder
           .search-field-holder
-            = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+            = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
             = icon("search", class: "search-icon")
           .dropdown
             - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 02efcecc8895687433128af2edb40279383bd374..fbe3ab912b615f1bd60c762511967a1baa943fa4 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,5 @@
 - grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
   - awards_sort(grouped_emojis).each do |emoji, awards|
     %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
       = emoji_icon(emoji, sprite: false)
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f7875e68b7e668745e10e238e7c7d1899b820a66..61c7cce20b24c5a9031474a1284e2e6e5a30f4e0 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -16,18 +16,20 @@
             %tr
               %td #{stage.capitalize} Job - #{build[:name]}
               %td
-                %pre
-                  = simple_format build[:commands]
+                %pre= build[:commands]
 
                 %br
                 %b Tag list:
-                = build[:tags]
+                = build[:tag_list].to_a.join(", ")
                 %br
                 %b Refs only:
-                = build[:only] && build[:only].join(", ")
+                = @jobs[build[:name].to_sym][:only].to_a.join(", ")
                 %br
                 %b Refs except:
-                = build[:except] && build[:except].join(", ")
+                = @jobs[build[:name].to_sym][:except].to_a.join(", ")
+                %br
+                %b Environment:
+                = build[:environment]
                 %br
                 %b When:
                 = build[:when]
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 0044d779c316728fabe30495b6dfb74c0a590038..889086c62b166e79a83945f5d80bcaa84f948394 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,3 +1,6 @@
+- page_title "CI Lint"
+- page_description "Validate your GitLab CI configuration file"
+
 %h2 Check your .gitlab-ci.yml
 %hr
 
diff --git a/app/views/dashboard/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f5222fe631e8c9ae988e09ea6afcf4144cc95857
--- /dev/null
+++ b/app/views/dashboard/groups/_empty_state.html.haml
@@ -0,0 +1,7 @@
+.groups-empty-state
+  = custom_icon("icon_empty_groups")
+
+  .text-content
+    %h4 A group is a collection of several projects.
+    %p If you organize your projects under a group, it works like a folder.
+    %p You can manage your group member’s permissions and access to each project in the group.
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index caca91af536d0be64f44792c092e386b5e6143c0..1a679c51774552f23e852311c8ca8ab6057cf14c 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,9 +2,12 @@
 - header_title  "Groups", dashboard_groups_path
 = render 'dashboard/groups_head'
 
-%ul.content-list
-  - @group_members.each do |group_member|
-    - group = group_member.group
-    = render 'shared/groups/group', group: group, group_member: group_member
+- 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'
+  = paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 0404d0728ea116a9adb8c409657112314058a623..bdea1064096b32f874a9ddcd660d6a6bf2af063d 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.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   "#{current_user.name} issues"
-  xml.link    href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  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?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 1eec4db45a07be6dce1bac2b8b3f58f28061d459..3caaf827ff5ad82559e7ea13c2aeb714fad24760 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -2,13 +2,13 @@
 - header_title  "Issues", issues_dashboard_path(assignee_id: current_user.id)
 = content_for :meta_tags do
   - if current_user
-    = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
+    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues")
 
 .top-area
   = render 'shared/issuable/nav', type: :issues
   .nav-controls
     - if current_user
-      = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+      = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
         = icon('rss')
         %span.icon-label
           Subscribe
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d4e7862981c2a3b9190e940d0c10544e205e639f..b2af438ea576706cc167915984753089f15d87fb 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -4,10 +4,10 @@
 = render 'dashboard/snippets_head'
 
 .nav-block
-  .controls
-    = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
+  .controls.hidden-xs
+    = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
       = icon('plus')
-      New Snippet
+      New snippet
 
   .nav-links.snippet-scope-menu
     %li{ class: ("active" unless params[:scope]) }
@@ -34,5 +34,9 @@
         %span.badge
           = current_user.snippets.are_public.count
 
-= render 'snippets/snippets'
+    .visible-xs
+      = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+        = icon('plus')
+        New snippet
 
+= render 'snippets/snippets'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index b40395c74ded7e8812163e2aa450c0251f6382c9..cc077fad32ab805af72a2839258533e9292bfd81 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -19,6 +19,7 @@
           (removed)
 
       &middot; #{time_ago_with_tooltip(todo.created_at)}
+      = todo_due_date(todo)
 
     .todo-body
       .todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4e340b6ec16f3dad72ee06eb91ab93c8a717af5b..5b2465e25ee75d751919a6b5b16d2d344f6377cb 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -1,53 +1,77 @@
 - page_title "Todos"
 - header_title "Todos", dashboard_todos_path
 
-.top-area
-  %ul.nav-links
-    - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
-    %li{class: "todos-pending #{todo_pending_active}"}
-      = link_to todos_filter_path(state: 'pending') do
-        %span
-          To do
-        %span.badge
-          = number_with_delimiter(todos_pending_count)
-    - todo_done_active = ('active' if params[:state] == 'done')
-    %li{class: "todos-done #{todo_done_active}"}
-      = link_to todos_filter_path(state: 'done') do
-        %span
-          Done
-        %span.badge
-          = number_with_delimiter(todos_done_count)
+- if current_user.todos.any?
+  .top-area
+    %ul.nav-links
+      - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+      %li{class: "todos-pending #{todo_pending_active}"}
+        = link_to todos_filter_path(state: 'pending') do
+          %span
+            To do
+          %span.badge
+            = number_with_delimiter(todos_pending_count)
+      - todo_done_active = ('active' if params[:state] == 'done')
+      %li{class: "todos-done #{todo_done_active}"}
+        = link_to todos_filter_path(state: 'done') do
+          %span
+            Done
+          %span.badge
+            = number_with_delimiter(todos_done_count)
 
-  .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
-        Mark all as done
-        = icon('spinner spin')
+    .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
+          Mark all as done
+          = icon('spinner spin')
+
+  .todos-filters
+    .row-content-block.second-block
+      = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+        .filter-item.inline
+          - if params[:project_id].present?
+            = hidden_field_tag(:project_id, params[:project_id])
+          = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+            placeholder: 'Search projects', data: { data: todo_projects_options } })
+        .filter-item.inline
+          - if params[:author_id].present?
+            = hidden_field_tag(:author_id, params[:author_id])
+          = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+            placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
+        .filter-item.inline
+          - if params[:type].present?
+            = hidden_field_tag(:type, params[:type])
+          = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+            data: { data: todo_types_options } })
+        .filter-item.inline.actions-filter
+          - if params[:action_id].present?
+            = hidden_field_tag(:action_id, params[:action_id])
+          = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+            data: { data: todo_actions_options }})
+        .pull-right
+          .dropdown.inline.prepend-left-10
+            %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+              %span.light
+              - if @sort.present?
+                = sort_options_hash[@sort]
+              - else
+                = sort_title_recently_created
+              = icon('caret-down')
+            %ul.dropdown-menu.dropdown-menu-align-right.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_recently_created) do
+                  = sort_title_recently_created
+                = link_to todos_filter_path(sort: sort_value_oldest_created) do
+                  = sort_title_oldest_created
 
-.todos-filters
-  .row-content-block.second-block
-    = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
-      .filter-item.inline
-        = select_tag('project_id', todo_projects_options,
-          class: 'select2 trigger-submit', include_blank: true,
-          data: {placeholder: 'Project'})
-      .filter-item.inline
-        = users_select_tag(:author_id, selected: params[:author_id],
-          placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
-      .filter-item.inline
-        = select_tag('type', todo_types_options,
-          class: 'select2 trigger-submit', include_blank: true,
-          data: {placeholder: 'Type'})
-      .filter-item.inline.actions-filter
-        = select_tag('action_id', todo_actions_options,
-          class: 'select2 trigger-submit', include_blank: true,
-          data: {placeholder: 'Action'})
 
 .prepend-top-default
   - if @todos.any?
     .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
     - @todos.group_by(&:project).each do |group|
-      .panel.panel-default.panel-small.js-todos-list
+      .panel.panel-default.panel-small
         - project = group[0]
         .panel-heading
           = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
@@ -55,13 +79,33 @@
         %ul.content-list.todos-list
           = render group[1]
     = paginate @todos, theme: "gitlab"
+  - elsif current_user.todos.any?
+    .todos-all-done
+      = render "shared/empty_states/todos_all_done.svg"
+      - if todos_filter_empty?
+        %h4.text-center
+          Good job! Looks like you don't have any todos left.
+        %p.text-center
+          Are you looking for things to do? Take a look at
+          = succeed "," do
+            = link_to "the opened issues", issues_dashboard_path
+          contribute to
+          = link_to "merge requests", merge_requests_dashboard_path
+          or mention someone in a comment to assign a new todo automatically.
+      - else
+        %h4.text-center
+          There are no todos to show.
   - else
-    .nothing-here-block You're all done!
-
-:javascript
-  new UsersSelect();
-
-  $('form.filter-form').on('submit', function (event) {
-    event.preventDefault();
-    Turbolinks.visit(this.action + '&' + $(this).serialize());
-  });
+    .todos-empty
+      .todos-empty-hero
+        = render "shared/empty_states/todos_empty.svg"
+      .todos-empty-content
+        %h4
+          Todos let you see what you should do next.
+        %p
+          When an issue or merge request is assigned to you, or when you
+          %strong
+            @mention
+          in a comment, this will trigger a new item in your todo list, automatically.
+        %p
+          You will always know what to work on next.
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 73c3a3dd2eb5f5337b8a95fb363331de7b648e81..20cd7b0179ddb4d64f02215d3d2899a1efa1a458 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,9 +3,9 @@
     Almost there...
   %p.lead
     Please check your email to confirm your account
-- if after_sign_up_text.present?
+- if current_application_settings.after_sign_up_text.present?
   .well-confirmation.text-center
-    = markdown(after_sign_up_text)
+    = markdown_field(current_application_settings, :after_sign_up_text)
 %p.confirmation-content.text-center
   No confirmation email received? Please check your spam folder or
 .append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 970ba1471118bfb89a7e2fa6545ede4a72092531..73e70dc63e56355e8310582acafb6d7642699a9b 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -1,14 +1,14 @@
+= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
 .login-box
-  .login-heading
-    %h3 Resend confirmation instructions
   .login-body
-    = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f|
+    = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
       .devise-errors
         = devise_error_messages!
-      .clearfix.append-bottom-20
-        = f.email_field :email, placeholder: 'Email', class: "form-control", required: true
+      .form-group
+        = f.label :email
+        = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.'
       .clearfix
-        = f.submit "Resend confirmation instructions", class: 'btn btn-success'
+        = f.submit "Resend", class: 'btn btn-success'
 
 .clearfix.prepend-top-20
   = render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 56048e99c17f834100138efe080de281fb875821..5e189e6dc544af58514ed2ae1654d5262e0d83c0 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -1,19 +1,21 @@
+= render 'devise/shared/tab_single', tab_title:'Change your password'
 .login-box
-  .login-heading
-    %h3 Change your password
   .login-body
-    = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
+    = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
       .devise-errors
         = devise_error_messages!
       = f.hidden_field :reset_password_token
-      %div
-        = f.password_field :password, class: "form-control top", placeholder: "New password", required: true
-      %div
-        = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true
+      .form-group
+        = f.label 'New password', for: :password
+        = f.password_field :password, class: "form-control top",  required: true, title: 'This field is required'
+      .form-group
+        = f.label 'Confirm new password', for: :password_confirmation
+        = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true
       .clearfix
         = f.submit "Change your password", class: "btn btn-primary"
 
 .clearfix.prepend-top-20
   %p
-    = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
-  = render 'devise/shared/sign_in_link'
+    %span.light Didn't receive a confirmation email?
+    = link_to "Request a new one", new_confirmation_path(resource_name)
+= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 535e85869e54564a5b08eb24c7b836521405873a..99ce13adf7408f47e00a27c2e32b5b00396ec0be 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -1,12 +1,12 @@
+= render 'devise/shared/tab_single', tab_title: 'Reset Password'
 .login-box
-  .login-heading
-    %h3 Reset password
   .login-body
-    = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
+    = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
       .devise-errors
         = devise_error_messages!
-      .clearfix.append-bottom-20
-        = f.email_field :email, placeholder: "Email",  class: "form-control", required: true, value: params[:user_email], autofocus: true
+      .form-group
+        = f.label :email
+        = f.email_field :email, class: "form-control", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
       .clearfix
         = f.submit "Reset password", class: "btn-primary btn"
 
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 9f5520603cdbe0fb6c14ee0c2428c05487cb82cc..21b895808189e9210f319f786d71b91606922b3b 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,12 +1,16 @@
-= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
-  = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off"
-  = f.password_field :password, class: "form-control bottom", placeholder: "Password"
+= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
+  %div.form-group
+    = f.label "Username or email", for: :login
+    = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
+  %div.form-group
+    = f.label :password
+    = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
+  %div.submit-container.move-submit-down
+    = f.submit "Sign in", class: "btn btn-save"
   - if devise_mapping.rememberable?
     .remember-me.checkbox
       %label{for: "user_remember_me"}
         = f.check_box :remember_me
         %span Remember me
-      .pull-right
+      .pull-right.forgot-password
         = link_to "Forgot your password?", new_password_path(resource_name)
-  %div
-    = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index b7d3acac2b152e5c90c5e0e256fb380e317e8b21..a6cadbcbdff8470a95fb20b355eb6fce02e308bd 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,9 +1,13 @@
-= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do
-  = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"}
-  = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
+= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
+  .form-group
+    = label_tag :username, 'Username or email'
+    = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
+  .form-group
+    = label_tag :password
+    = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
   - if devise_mapping.rememberable?
     .remember-me.checkbox
       %label{for: "remember_me"}
         = check_box_tag :remember_me, '1', false, id: 'remember_me'
         %span Remember me
-  = button_tag "Sign in", class: "btn-save btn"
+  = submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 689cd6ed6654a0793a0f2b6b5c752a9418a89733..3ab5461f92933087e768d77953b5ec0e216f4e4c 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,9 +1,13 @@
-= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do
-  = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
-  = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
+= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
+  .form-group
+    = label_tag :username, "#{server['label']} Username"
+    = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
+  .form-group
+    = label_tag :password
+    = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
   - if devise_mapping.rememberable?
     .remember-me.checkbox
       %label{for: "remember_me"}
         = check_box_tag :remember_me, '1', false, id: 'remember_me'
         %span Remember me
-  = button_tag "Sign in", class: "btn-save btn"
+  = submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 28194506accff5f5eeef336c9a5a3db747c0a429..fa8e7979461dc8c25b77ecb3ba5a6ec6c3afc486 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,19 +1,22 @@
 - page_title "Sign in"
 %div
-  - if signin_enabled? || ldap_enabled? || crowd_enabled?
-    = render 'devise/shared/signin_box'
+  - if form_based_providers.any?
+    = render 'devise/shared/tabs_ldap'
+  - else
+    = render 'devise/shared/tabs_normal'
+  .tab-content
+    - if signin_enabled? || ldap_enabled? || crowd_enabled?
+      = render 'devise/shared/signin_box'
 
-  -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
-  - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
-    .clearfix.prepend-top-20
-      = render 'devise/shared/omniauth_box'
-
-  -# Signup only makes sense if you can also sign-in
-  - if signin_enabled? && signup_enabled?
-    .prepend-top-20
+    -# Signup only makes sense if you can also sign-in
+    - if signin_enabled? && signup_enabled?
       = render 'devise/shared/signup_box'
 
   -# Show a message if none of the mechanisms above are enabled
   - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
     %div
       No authentication methods configured.
+
+  - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
+    .clearfix
+      = render 'devise/shared/omniauth_box'
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 4debd3d608f822dcaecd7c8cea1dabd0bf54511a..2cadc424668a022e05099a8962f7968fe934171f 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -3,21 +3,19 @@
     = page_specific_javascript_tag('u2f.js')
 
 %div
+  = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
   .login-box
-    .login-heading
-      %h3 Two-Factor Authentication
     .login-body
       - if @user.two_factor_otp_enabled?
-        %h5 Authenticate via Two-Factor App
-        = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
+        = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f|
           - resource_params = params[resource_name].presence || params
           = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
-          = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
-          %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
-          .prepend-top-20
-            = f.submit "Verify code", class: "btn btn-save"
+          %div
+            = f.label 'Two-Factor Authentication code', name:  :otp_attempt
+            = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
+            %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
+            .prepend-top-20
+              = f.submit "Verify code", class: "btn btn-save"
 
       - if @user.two_factor_u2f_enabled?
-
-        %hr
-        = render "u2f/authenticate"
+        = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 2e7da2747d0a421f86a995dc1844a7d467842bf0..8908b64cdac9f7ccc7efae71d8c11008134d5151 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,8 +1,9 @@
-%p
-  %span.light
-    Sign in with &nbsp;
-  - providers = enabled_button_based_providers
-  - providers.each do |provider|
+%div.omniauth-container
+  %p
     %span.light
-      - has_icon = provider_has_icon?(provider)
-      = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+      Sign in with &nbsp;
+    - providers = enabled_button_based_providers
+    - providers.each do |provider|
+      %span.light
+        - has_icon = provider_has_icon?(provider)
+        = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index fafc4b82f5394cf258ddc2fda1310b153b9a3537..289bf40f3de41113aec2247385b146a228dde821 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,5 +1,4 @@
 %p
   %span.light
     Already have login and password?
-  %strong
     = link_to "Sign in", new_session_path(resource_name)
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 2c15e2c4891c0ab90e6b961a66deef3df24c143b..86edaf14e43287d8f3d54d7233dabc05ccf1672d 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,32 +1,18 @@
-.login-box
-  - if signup_enabled?
-    .login-heading
-      %h3 Existing user? Sign in
-  - else
-    .login-heading
-      %h3 Sign in
-  .login-body
-    - if form_based_providers.any?
-      %ul.nav-links
-        - if crowd_enabled?
-          %li.active
-            = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
-        - @ldap_servers.each_with_index do |server, i|
-          %li{class: (:active if i.zero? && !crowd_enabled?)}
-            = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
-        - if signin_enabled?
-          %li
-            = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
-      .tab-content
-        - if crowd_enabled?
-          %div.tab-pane.active{id: "tab-crowd"}
-            = render 'devise/sessions/new_crowd'
-        - @ldap_servers.each_with_index do |server, i|
-          %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
-            = render 'devise/sessions/new_ldap', server: server
-        - if signin_enabled?
-          %div#tab-signin.tab-pane
-            = render 'devise/sessions/new_base'
+- if form_based_providers.any?
+  - if crowd_enabled?
+    .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'}
+      .login-body
+        = render 'devise/sessions/new_crowd'
+  - @ldap_servers.each_with_index do |server, i|
+    .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)}
+      .login-body
+        = render 'devise/sessions/new_ldap', server: server
+  - if signin_enabled?
+    .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'}
+      .login-body
+        = render 'devise/sessions/new_base'
 
-    - elsif signin_enabled?
+- elsif signin_enabled?
+  .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'}
+    .login-body
       = render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 905a8dbcd841ac80d82f56fe42916da0f53d7d85..7c68e3266e5177ffecbdf37e516d4e05af76fefb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,29 +1,30 @@
-.login-box
-  - if signin_enabled?
-    .login-heading
-      %h3 New user? Create an account
-  - else
-    .login-heading
-      %h3 Create an account
+#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
   .login-body
-    = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f|
+    = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
       .devise-errors
         = devise_error_messages!
-      %div
-        = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
-      %div
-        = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
-      %div
-        = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
+      %div.form-group
+        = f.label :name
+        = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
+      %div.username.form-group
+        = f.label :username
+        = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", 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...
+      %div.form-group
+        = f.label :email
+        = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
       .form-group.append-bottom-20#password-strength
-        = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters"
+        = f.label :password
+        = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
+        %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
       %div
       - if current_application_settings.recaptcha_enabled
         = recaptcha_tags
       %div
-        = f.submit "Sign up", class: "btn-create btn"
-
-.clearfix.prepend-top-20
+        = f.submit "Register", class: "btn-register btn"
+.clearfix.submit-container
   %p
     %span.light Didn't receive a confirmation email?
     = succeed '.' do
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f943d25e41a78e7abca023fbaae3fb0ae107df5c
--- /dev/null
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -0,0 +1,3 @@
+%ul.nav-links.nav-tabs.new-session-tabs.single-tab
+  %li.active
+    %a= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..1e957f0935f369b8df870aee0217a2ff75b53727
--- /dev/null
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -0,0 +1,10 @@
+%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+  - if crowd_enabled?
+    %li.active
+      = link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
+  - @ldap_servers.each_with_index do |server, i|
+    %li{class: (:active if i.zero? && !crowd_enabled?)}
+      = link_to server['label'], "##{server['provider_name']}",  'data-toggle' => 'tab'
+  - if signin_enabled?
+    %li
+      = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..05246303fb6ee17d10e604e695e94c3b8829a77f
--- /dev/null
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -0,0 +1,6 @@
+%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
+  %li.active{ role: 'presentation' }
+    %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
+  - if signin_enabled? && signup_enabled?
+    %li{ role: 'presentation'}
+      %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
index 49c087c0646e42112a6266225eb0ecf0aa225039..b2f48a4e0bf252cec61cb132c0a2055d1094595d 100644
--- a/app/views/devise/unlocks/new.html.haml
+++ b/app/views/devise/unlocks/new.html.haml
@@ -1,12 +1,12 @@
+= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
 .login-box
-  .login-heading
-    %h3 Resend unlock email
   .login-body
-    = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
+    = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
       .devise-errors
         = devise_error_messages!
-      .clearfix.append-bottom-20
-        = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
+      .form-group.append-bottom-20
+        = f.label :email
+        = f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.'
       .clearfix
         = f.submit 'Resend unlock instructions', class: 'btn btn-success'
 
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index fa1ad9efa73c1ea9baef59536faa74a591b16df3..1411daeb4a69187730403a621f021bbd00d18541 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -1,6 +1,6 @@
-%tr.notes_holder
+- expanded = local_assigns.fetch(:expanded, true)
+%tr.notes_holder{class: ('hide' unless expanded)}
   %td.notes_line{ colspan: 2 }
   %td.notes_content
-    %ul.notes{ data: { discussion_id: discussion.id } }
-      = render partial: "projects/notes/note", collection: discussion.notes, as: :note
-    = link_to_reply_discussion(discussion)
+    .content
+      = render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 02b159ffd454b4f2b19bfc407d08723c03dce167..3a95a65281008c7d838a78207e665185df7b9095 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,8 +7,11 @@
 
   .diff-content.code.js-syntax-highlight
     %table
-      - discussion.truncated_diff_lines.each do |line|
-        = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
-
-        - if discussion.for_line?(line)
-          = render "discussions/diff_discussion", discussion: discussion
+      - discussions = { discussion.original_line_code => discussion }
+      = render partial: "projects/diffs/line",
+        collection: discussion.truncated_diff_lines,
+        as: :line,
+        locals: { diff_file: diff_file,
+          discussions: discussions,
+          discussion_expanded: true,
+          plain: true }
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 49702e048aa717fffac79c383e4ed626c48251aa..077e8e64e5fbaa328982d5a15e6da71c841ef342 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -5,8 +5,17 @@
       = link_to user_path(discussion.author) do
         = image_tag avatar_icon(discussion.author), class: "avatar s40"
     .timeline-content
-      .discussion.js-toggle-container{ class: discussion.id }
+      .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
         .discussion-header
+          .discussion-actions
+            = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
+              - if expanded
+                = icon("chevron-up")
+              - else
+                = icon("chevron-down")
+
+              Toggle discussion
+
           = link_to_member(@project, discussion.author, avatar: false)
 
           .inline.discussion-headline-light
@@ -29,17 +38,11 @@
 
             = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
 
-          .discussion-actions
-            = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
-              - if expanded
-                = icon("chevron-up")
-              - else
-                = icon("chevron-down")
-
-              Toggle discussion
+          = render "discussions/headline", discussion: discussion
 
         .discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
           - if discussion.diff_discussion? && discussion.diff_file
             = render "discussions/diff_with_notes", discussion: discussion
           - else
-            = render "discussions/notes", discussion: discussion
+            .panel.panel-default
+              = render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c1dabeed3871a3cb549e513bd9ed0bc432c09c9f
--- /dev/null
+++ b/app/views/discussions/_headline.html.haml
@@ -0,0 +1,14 @@
+- if discussion.resolved?
+  .discussion-headline-light.js-discussion-headline
+    Resolved
+    - if discussion.resolved_by
+      by
+      = link_to_member(@project, discussion.resolved_by, avatar: false)
+    = time_ago_with_tooltip(discussion.resolved_at, placement: "bottom")
+- elsif discussion.last_updated_at != discussion.created_at
+  .discussion-headline-light.js-discussion-headline
+    Last updated
+    - if discussion.last_updated_by
+      by
+      = link_to_member(@project, discussion.last_updated_by, avatar: false)
+    = time_ago_with_tooltip(discussion.last_updated_at, placement: "bottom")
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7ed09dd1a9874b323c79ae655e435a4b51878a30
--- /dev/null
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -0,0 +1,9 @@
+- discussion = local_assigns.fetch(:discussion, nil)
+- if current_user
+  %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
+    .btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
+      %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
+        title: "Jump to next unresolved discussion",
+        "aria-label" => "Jump to next unresolved discussion",
+        data: { container: "body" }}
+        = custom_icon("next_discussion")
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index a2642b839f6cacb61cd64f72afd0e62bae9ac7fc..dfdbdf1f969ce98942c210b9a5c0a8aea5665209 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,5 +1,16 @@
-.panel.panel-default
-  .notes{ data: { discussion_id: discussion.id } }
-    %ul.notes.timeline
-      = render partial: "projects/notes/note", collection: discussion.notes, as: :note
-  = link_to_reply_discussion(discussion)
+%ul.notes{ data: { discussion_id: discussion.id } }
+  = render partial: "projects/notes/note", collection: discussion.notes, as: :note
+
+- if current_user
+  .discussion-reply-holder
+    - if discussion.diff_discussion?
+      - line_type = local_assigns.fetch(:line_type, nil)
+
+      .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
+        .btn-group{ role: "group" }
+          = 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
+    - else
+      = link_to_reply_discussion(discussion)
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index a798c438ea0e9c26dfe764ff7bc887529069db93..f1072ce0febaefdac9514ed35559a2bc27a21512 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,22 +1,21 @@
-%tr.notes_holder
+- expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?)
+%tr.notes_holder{class: ('hide' unless expanded)}
   - if discussion_left
     %td.notes_line.old
     %td.notes_content.parallel.old
-      %ul.notes{ data: { discussion_id: discussion_left.id } }
-        = render partial: "projects/notes/note", collection: discussion_left.notes, as: :note
-
-      = link_to_reply_discussion(discussion_left, 'old')
+      .content{class: ('hide' unless discussion_left.expanded?)}
+        = render "discussions/notes", discussion: discussion_left, line_type: 'old'
   - else
     %td.notes_line.old= ""
-    %td.notes_content.parallel.old= ""
+    %td.notes_content.parallel.old
+      .content
 
   - if discussion_right
     %td.notes_line.new
     %td.notes_content.parallel.new
-      %ul.notes{ data: { discussion_id: discussion_right.id } }
-        = render partial: "projects/notes/note", collection: discussion_right.notes, as: :note
-
-      = link_to_reply_discussion(discussion_right, 'new')
+      .content{class: ('hide' unless discussion_right.expanded?)}
+        = render "discussions/notes", discussion: discussion_right, line_type: 'new'
   - else
     %td.notes_line.new= ""
-    %td.notes_content.parallel.new= ""
+    %td.notes_content.parallel.new
+      .content
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f0b61e0f7de3812ae21a85147f83055f965bff30
--- /dev/null
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -0,0 +1,10 @@
+- if discussion.for_merge_request?
+  %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
+      ":discussion-id" => "'#{discussion.id}'",
+      ":merge-request-id" => discussion.noteable.iid,
+      ":can-resolve" => discussion.can_resolve?(current_user),
+      "inline-template" => true }
+    .btn-group{ role: "group", "v-if" => "showButton" }
+      %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
+        = icon("spinner spin", "v-show" => "loading")
+        {{ buttonText }}
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index bfa95ce79a7c94695c4649e1c64db4ad31540382..9f02a8d2ed9d6d7af6ca26666634d21003f7385c 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -6,4 +6,4 @@
   
 = form_tag path do
   %input{:name => "_method", :type => "hidden", :value => "delete"}/
-  = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm'
+  = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 57f6e7e0612646983d79cab934c96d0f3852efda..a1b39d9e1a0cf2e973c88a5112437479a184d6f5 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -23,8 +23,8 @@
           = sort_options_hash[@sort]
         - else
           = sort_title_recently_created
-        %b.caret
-      %ul.dropdown-menu
+        = icon('caret-down')
+      %ul.dropdown-menu.dropdown-menu-align-right
         %li
           = link_to explore_groups_path(sort: sort_value_recently_created) do
             = sort_title_recently_created
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index cd485da5104f77aec9fd42ba85f4b388593b4a20..4cff14b096b36243346cb840364de8f6fca7c946 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -7,8 +7,8 @@
         = visibility_level_label(params[:visibility_level].to_i)
       - else
         Any
-      %b.caret
-    %ul.dropdown-menu
+      = icon('caret-down')
+    %ul.dropdown-menu.dropdown-menu-align-right
       %li
         = link_to filter_projects_path(visibility_level: nil) do
           Any
@@ -27,8 +27,8 @@
         = params[:tag]
       - else
         Any
-      %b.caret
-    %ul.dropdown-menu
+      = icon('caret-down')
+    %ul.dropdown-menu.dropdown-menu-align-right
       %li
         = link_to filter_projects_path(tag: nil) do
           Any
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 6306fe6d0bfca90499ff83b1ef164e040caa599a..7def9eacdc9f3e11d57c26aad69b254e22150dc5 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -8,9 +8,8 @@
 
 .row-content-block
   - if current_user
-    .pull-right
-      = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
-        New Snippet
+    = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+      New snippet
 
   .oneline
     Public snippets created by you and other users are listed here
diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..af57065f0fc59a2c6366b29759603468e3950dc8
--- /dev/null
+++ b/app/views/groups/_group_lfs_settings.html.haml
@@ -0,0 +1,11 @@
+- if current_user.admin?
+  .form-group
+    .col-sm-offset-2.col-sm-10
+      .checkbox
+        = f.label :lfs_enabled do
+          = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+          %strong
+            Allow projects within this group to use Git LFS
+            = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+          %br/
+          %span.descr This setting can be overridden in each project.
\ No newline at end of file
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index decb89b2fd60936c02cc5ab27eaa97be0ba355bc..2706e8692d16793c3a707f6d0abc712c5c7f056c 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -2,13 +2,14 @@
   .panel-heading
     Group settings
   .panel-body
-    = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
+    = form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f|
       = form_errors(@group)
       = render 'shared/group_form', f: f
 
       .form-group
         .col-sm-offset-2.col-sm-10
-          = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
+          .avatar-container.s160
+            = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
           %p.light
             - if @group.avatar?
               You can change your group avatar here
@@ -25,6 +26,8 @@
         .col-sm-offset-2.col-sm-10
           = render 'shared/allow_request_access', form: f
 
+      = render 'group_lfs_settings', f: f
+
       .form-group
         %hr
         = f.label :share_with_group_lock, class: 'control-label' do
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 9bb9f96217770455302e3e0b5a0022f442ccaf27..b185b81db7ff5d2cee606034cdb82e0762520fd2 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -1,18 +1,22 @@
-= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
-  .form-group
-    = f.label :user_ids, "People", class: 'control-label'
-    .col-sm-10
-      = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
-      .help-block
+= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
+  .row
+    .col-md-4.col-lg-6
+      = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
+      .help-block.append-bottom-10
         Search for users by name, username, or email, or invite new ones using their email address.
 
-  .form-group
-    = f.label :access_level, "Group Access", class: 'control-label'
-    .col-sm-10
-      = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2"
-      .help-block
-        Read more about role permissions
-        %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+    .col-md-3.col-lg-2
+      = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
+      .help-block.append-bottom-10
+        = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+        about role permissions
 
-  .form-actions
-    = f.submit 'Add users to group', class: "btn btn-create"
+    .col-md-3.col-lg-2
+      .clearable-input
+        = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+        %i.clear-icon.js-clear-input
+      .help-block.append-bottom-10
+        On this date, the user(s) will automatically lose access to this group and all of its projects.
+
+    .col-md-2
+      = f.submit 'Add to group', class: "btn btn-create btn-block"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 90f362c052be70f24ba822a0d6e8bfaa02cbf5dd..ebf9aca7700849b90e958f0a851fa37c9f6fcafc 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,35 +1,31 @@
 - page_title "Members"
 
-.group-members-page.prepend-top-default
+.project-members-page.prepend-top-default
+  %h4
+    Members
+  %hr
   - if can?(current_user, :admin_group_member, @group)
-    .panel.panel-default
-      .panel-heading
-        Add new user to group
-      .panel-body
-        %p.light
-          Members of group have access to all group projects.
-        .new-group-member-holder
-          = render "new_group_member"
+    .project-members-new.append-bottom-default
+      %p.clearfix
+        Add new user to
+        %strong= @group.name
+      = render "new_group_member"
 
     = render 'shared/members/requests', membership_source: @group, requesters: @requesters
 
+  .append-bottom-default.clearfix
+    %h5.member.existing-title
+      Existing users
+    = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form'  do
+      .form-group
+        = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+        %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+          = icon("search")
   .panel.panel-default
     .panel-heading
+      Users with access to
       %strong #{@group.name}
-      group members
-      %span.badge= @members.size
-      .controls
-        = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form'  do
-          .form-group
-            = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
-          = button_tag class: 'btn', title: 'Search' do
-            = icon("search")
+      %span.badge= @members.total_count
     %ul.content-list
       = render partial: 'shared/members/member', collection: @members, as: :member
     = paginate @members, theme: 'gitlab'
-
-:javascript
-  $('form.member-search-form').on('submit', function(event) {
-    event.preventDefault();
-    Turbolinks.visit(this.action + '?' + $(this).serialize());
-  });
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e52ec2e6e940404770e62d4238905d..de8f53b6b52ba27088c628c6970303ace0a6fbc7 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
 :plain
-  $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+  var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+  $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index b16280403253f890204ff6b74dba591caacc3c70..0cc6466d34eadaaa16348e7e9b7c9fb60ac00597 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.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} issues"
-  xml.link    href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  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?
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 4434f1cbd356388cf31b0701f4253bf1802c7657..dc6c1bb69de9fdd8d2e067095b1def6b8dd98912 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,13 +1,13 @@
 - page_title "Issues"
 = content_for :meta_tags do
   - if current_user
-    = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
+    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
 
 .top-area
   = render 'shared/issuable/nav', type: :issues
   .nav-controls
     - if current_user
-      = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+      = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
         = icon('rss')
         %span.icon-label
           Subscribe
diff --git a/app/views/groups/labels/destroy.js.haml b/app/views/groups/labels/destroy.js.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3dfbfc77c0d4c18f6c77fa2201fb3ce57e09ac21
--- /dev/null
+++ b/app/views/groups/labels/destroy.js.haml
@@ -0,0 +1,2 @@
+- if @group.labels.empty?
+  $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..836981fc6fde122c2870805077854085432cf259
--- /dev/null
+++ b/app/views/groups/labels/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title 'Edit', @label.name, 'Labels'
+
+%h3.page-title
+  Edit Label
+%hr
+
+= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..45325d6bc4b95f39be28c7ef1098c269cd5a64b4
--- /dev/null
+++ b/app/views/groups/labels/index.html.haml
@@ -0,0 +1,20 @@
+- page_title 'Labels'
+
+.top-area.adjust
+  .nav-text
+    Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
+
+  .nav-controls
+    - if can?(current_user, :admin_label, @group)
+      = link_to new_group_label_path(@group), class: "btn btn-new" do
+        New label
+
+.labels
+  .other-labels
+    - if @labels.present?
+      %ul.content-list.manage-labels-list.js-other-labels
+        = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
+        = paginate @labels, theme: 'gitlab'
+    - else
+      .nothing-here-block
+        No labels created yet.
diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2be87460b1d9f55be352edcad39b21672474c121
--- /dev/null
+++ b/app/views/groups/labels/new.html.haml
@@ -0,0 +1,8 @@
+- page_title 'New Label'
+- header_title group_title(@group, 'Labels', group_labels_path(@group))
+
+%h3.page-title
+  New Label
+%hr
+
+= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index ca6c4326d1c38a3c19ba3b68257fa41bd1e6c5cf..23d438b2aa12c3000a13d81417d744045dd4df7a 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -33,8 +33,8 @@
       .form-group
         = f.label :projects, "Projects", class: "control-label"
         .col-sm-10
-          = f.collection_select :project_ids, @group.projects, :id, :name,
-            { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+          = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
+            { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
 
     .col-md-6
       .form-group
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 2b8bc269e645c117c15955469075b156f3406c77..d19eaa6add943ba7e0bc06e8bd7c18b35d741f73 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -5,7 +5,7 @@
   New Group
 %hr
 
-= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
+= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f|
   = form_errors(@group)
   = render 'shared/group_form', f: f, autofocus: true
 
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 53ed4fa991d688264891957c785f000808bc1924..b439b40a75abbbc77a77f05dbabaa4f8d5582e1c 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,7 +6,8 @@
 
 .cover-block.groups-cover-block
   %div{ class: container_class }
-    = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile"
+    .avatar-container.s70.group-avatar
+      = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
     .group-info
       .cover-title
         %h1
@@ -21,9 +22,9 @@
 
       - if @group.description.present?
         .cover-desc.description
-          = markdown(@group.description, pipeline: :description)
+          = markdown_field(@group, :description)
 
-%div{ class: container_class }
+%div.groups-header{ class: container_class }
   .top-area
     %ul.nav-links
       %li.active
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index ce4536ebdc630fb09eeb0a100bdaa8c38e9fe073..65842a0479b8cdb00853d2d7d8d51dff96028086 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -7,277 +7,284 @@
           Keyboard Shortcuts
           %small
             = link_to '(Show all)', '#', class: 'js-more-help-button'
-      .modal-body.shortcuts-cheatsheet
-        .col-lg-4
-          %table.shortcut-mappings
-            %tbody
-              %tr
-                %th
-                %th Global Shortcuts
-              %tr
-                %td.shortcut
-                  .key s
-                %td Focus Search
-              %tr
-                %td.shortcut
-                  .key f
-                %td Focus Filter
-              %tr
-                %td.shortcut
-                  .key ?
-                %td Show/hide this dialog
-              %tr
-                %td.shortcut
-                  - if browser.platform.mac?
-                    .key &#8984; shift p
-                  - else
-                    .key ctrl shift p
-                %td Toggle Markdown preview
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-up
-                %td Edit last comment (when focused on an empty textarea)
-            %tbody
-              %tr
-                %th
-                %th Project Files browsing
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-up
-                %td Move selection up
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-down
-                %td Move selection down
-              %tr
-                %td.shortcut
-                  .key enter
-                %td Open Selection
-            %tbody
-              %tr
-                %th
-                %th Finding Project File
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-up
-                %td Move selection up
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-down
-                %td Move selection down
-              %tr
-                %td.shortcut
-                  .key enter
-                %td Open Selection
-              %tr
-                %td.shortcut
-                  .key esc
-                %td Go back
+      .modal-body
+        .row
+          .col-lg-4
+            %table.shortcut-mappings
+              %tbody
+                %tr
+                  %th
+                  %th Global Shortcuts
+                %tr
+                  %td.shortcut
+                    .key s
+                  %td Focus Search
+                %tr
+                  %td.shortcut
+                    .key f
+                  %td Focus Filter
+                %tr
+                  %td.shortcut
+                    .key ?
+                  %td Show/hide this dialog
+                %tr
+                  %td.shortcut
+                    - if browser.platform.mac?
+                      .key &#8984; shift p
+                    - else
+                      .key ctrl shift p
+                  %td Toggle Markdown preview
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-up
+                  %td Edit last comment (when focused on an empty textarea)
+              %tbody
+                %tr
+                  %th
+                  %th Project Files browsing
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-up
+                  %td Move selection up
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-down
+                  %td Move selection down
+                %tr
+                  %td.shortcut
+                    .key enter
+                  %td Open Selection
+              %tbody
+                %tr
+                  %th
+                  %th Finding Project File
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-up
+                  %td Move selection up
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-down
+                  %td Move selection down
+                %tr
+                  %td.shortcut
+                    .key enter
+                  %td Open Selection
+                %tr
+                  %td.shortcut
+                    .key esc
+                  %td Go back
 
-        .col-lg-4
-          %table.shortcut-mappings
-            %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
-              %tr
-                %th
-                %th Global Dashboard
-              %tr
-                %td.shortcut
-                  .key g
-                  .key a
-                %td
-                  Go to the activity feed
-              %tr
-                %td.shortcut
-                  .key g
-                  .key p
-                %td
-                  Go to projects
-              %tr
-                %td.shortcut
-                  .key g
-                  .key i
-                %td
-                  Go to issues
-              %tr
-                %td.shortcut
-                  .key g
-                  .key m
-                %td
-                  Go to merge requests
-            %tbody
-              %tr
-                %th
-                %th Project
-              %tr
-                %td.shortcut
-                  .key g
-                  .key p
-                %td
-                  Go to the project's home page
-              %tr
-                %td.shortcut
-                  .key g
-                  .key e
-                %td
-                  Go to the project's activity feed
-              %tr
-                %td.shortcut
-                  .key g
-                  .key f
-                %td
-                  Go to files
-              %tr
-                %td.shortcut
-                  .key g
-                  .key c
-                %td
-                  Go to commits
-              %tr
-                %td.shortcut
-                  .key g
-                  .key b
-                %td
-                  Go to builds
-              %tr
-                %td.shortcut
-                  .key g
-                  .key n
-                %td
-                  Go to network graph
-              %tr
-                %td.shortcut
-                  .key g
-                  .key g
-                %td
-                  Go to graphs
-              %tr
-                %td.shortcut
-                  .key g
-                  .key i
-                %td
-                  Go to issues
-              %tr
-                %td.shortcut
-                  .key g
-                  .key m
-                %td
-                  Go to merge requests
-              %tr
-                %td.shortcut
-                  .key g
-                  .key s
-                %td
-                  Go to snippets
-              %tr
-                %td.shortcut
-                  .key t
-                %td Go to finding file
-              %tr
-                %td.shortcut
-                  .key i
-                %td New issue
-        .col-lg-4
-          %table.shortcut-mappings
-            %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
-              %tr
-                %th
-                %th Network Graph
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-left
-                  \/
-                  .key h
-                %td Scroll left
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-right
-                  \/
-                  .key l
-                %td Scroll right
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-up
-                  \/
-                  .key k
-                %td Scroll up
-              %tr
-                %td.shortcut
-                  .key
-                    %i.fa.fa-arrow-down
-                  \/
-                  .key j
-                %td Scroll down
-              %tr
-                %td.shortcut
-                  .key
-                    shift
-                    %i.fa.fa-arrow-up
-                  \/
-                  .key
-                    shift k
-                %td Scroll to top
-              %tr
-                %td.shortcut
-                  .key
-                    shift
-                    %i.fa.fa-arrow-down
-                  \/
-                  .key
-                    shift j
-                %td Scroll to bottom
-            %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
-              %tr
-                %th
-                %th Issues
-              %tr
-                %td.shortcut
-                  .key a
-                %td Change assignee
-              %tr
-                %td.shortcut
-                  .key m
-                %td Change milestone
-              %tr
-                %td.shortcut
-                  .key r
-                %td Reply (quoting selected text)
-              %tr
-                %td.shortcut
-                  .key e
-                %td Edit issue
-              %tr
-                %td.shortcut
-                  .key l
-                %td Change Label
-            %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
-              %tr
-                %th
-                %th Merge Requests
-              %tr
-                %td.shortcut
-                  .key a
-                %td Change assignee
-              %tr
-                %td.shortcut
-                  .key m
-                %td Change milestone
-              %tr
-                %td.shortcut
-                  .key r
-                %td Reply (quoting selected text)
-              %tr
-                %td.shortcut
-                  .key e
-                %td Edit merge request
-              %tr
-                %td.shortcut
-                  .key l
-                %td Change Label
+          .col-lg-4
+            %table.shortcut-mappings
+              %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+                %tr
+                  %th
+                  %th Global Dashboard
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key a
+                  %td
+                    Go to the activity feed
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key p
+                  %td
+                    Go to projects
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key i
+                  %td
+                    Go to issues
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key m
+                  %td
+                    Go to merge requests
+              %tbody
+                %tr
+                  %th
+                  %th Project
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key p
+                  %td
+                    Go to the project's home page
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key e
+                  %td
+                    Go to the project's activity feed
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key f
+                  %td
+                    Go to files
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key c
+                  %td
+                    Go to commits
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key b
+                  %td
+                    Go to builds
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key n
+                  %td
+                    Go to network graph
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key g
+                  %td
+                    Go to graphs
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key i
+                  %td
+                    Go to issues
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key l
+                  %td
+                    Go to issue boards
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key m
+                  %td
+                    Go to merge requests
+                %tr
+                  %td.shortcut
+                    .key g
+                    .key s
+                  %td
+                    Go to snippets
+                %tr
+                  %td.shortcut
+                    .key t
+                  %td Go to finding file
+                %tr
+                  %td.shortcut
+                    .key i
+                  %td New issue
+          .col-lg-4
+            %table.shortcut-mappings
+              %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+                %tr
+                  %th
+                  %th Network Graph
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-left
+                    \/
+                    .key h
+                  %td Scroll left
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-right
+                    \/
+                    .key l
+                  %td Scroll right
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-up
+                    \/
+                    .key k
+                  %td Scroll up
+                %tr
+                  %td.shortcut
+                    .key
+                      %i.fa.fa-arrow-down
+                    \/
+                    .key j
+                  %td Scroll down
+                %tr
+                  %td.shortcut
+                    .key
+                      shift
+                      %i.fa.fa-arrow-up
+                    \/
+                    .key
+                      shift k
+                  %td Scroll to top
+                %tr
+                  %td.shortcut
+                    .key
+                      shift
+                      %i.fa.fa-arrow-down
+                    \/
+                    .key
+                      shift j
+                  %td Scroll to bottom
+              %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+                %tr
+                  %th
+                  %th Issues
+                %tr
+                  %td.shortcut
+                    .key a
+                  %td Change assignee
+                %tr
+                  %td.shortcut
+                    .key m
+                  %td Change milestone
+                %tr
+                  %td.shortcut
+                    .key r
+                  %td Reply (quoting selected text)
+                %tr
+                  %td.shortcut
+                    .key e
+                  %td Edit issue
+                %tr
+                  %td.shortcut
+                    .key l
+                  %td Change Label
+              %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
+                %tr
+                  %th
+                  %th Merge Requests
+                %tr
+                  %td.shortcut
+                    .key a
+                  %td Change assignee
+                %tr
+                  %td.shortcut
+                    .key m
+                  %td Change milestone
+                %tr
+                  %td.shortcut
+                    .key r
+                  %td Reply (quoting selected text)
+                %tr
+                  %td.shortcut
+                    .key e
+                  %td Edit merge request
+                %tr
+                  %td.shortcut
+                    .key l
+                  %td Change Label
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 57601ae9be0970a173f57489beba03099964052c..31631887317849fec46dd73c641251388508dd68 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -20,7 +20,7 @@
     Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
     - if current_application_settings.help_page_text.present?
       %hr
-      = markdown(current_application_settings.help_page_text)
+      = markdown_field(current_application_settings, :help_page_text)
 
 %hr
 
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 85e188d6f8b1da476eba729a0cf81c6a9ad8a61b..070ed90da6dfb0c86bfbd97de8f9bbe160135a6c 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -461,7 +461,7 @@
         .panel-body
           = lorem
 
-  %h2#alert Alerts
+  %h2#alerts Alerts
 
   .row
     .col-md-6
@@ -549,4 +549,4 @@
     %li wiki page
     %li help page
 
-  You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}.
+  You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}.
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 804ad88468f31c205c2c88d82f1a04a3c30fb12f..8e9295383516cd52b0764aac96d99914b235076d 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -1,23 +1,4 @@
-- if @already_been_taken
-  :plain
-    tr = $("tr#repo_#{@repo_id}")
-    target_field = tr.find(".import-target")
-    import_button = tr.find(".btn-import")
-    origin_target = target_field.text()
-    project_name = "#{@project_name}"
-    origin_namespace = "#{@target_namespace}"
-    target_field.empty()
-    target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>")
-    target_field.append("<input type='text' name='target_namespace' />")
-    target_field.append("/" + project_name)
-    target_field.data("project_name", project_name)
-    target_field.find('input').prop("value", origin_namespace)
-    import_button.enable().removeClass('is-loading')
-- elsif @access_denied
-  :plain
-    job = $("tr#repo_#{@repo_id}")
-    job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
-- elsif @project.persisted?
+- if @project.persisted?
   :plain
     job = $("tr#repo_#{@repo_id}")
     job.attr("id", "project_#{@project.id}")
diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml
new file mode 100644
index 0000000000000000000000000000000000000000..36f8069c1f7951696d0826640cfd447d0324badf
--- /dev/null
+++ b/app/views/import/base/unauthorized.js.haml
@@ -0,0 +1,14 @@
+:plain
+  tr = $("tr#repo_#{@repo_id}")
+  target_field = tr.find(".import-target")
+  import_button = tr.find(".btn-import")
+  origin_target = target_field.text()
+  project_name = "#{@project_name}"
+  origin_namespace = "#{@target_namespace.path}"
+  target_field.empty()
+  target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>")
+  target_field.append("<input type='text' name='target_namespace' />")
+  target_field.append("/" + project_name)
+  target_field.data("project_name", project_name)
+  target_field.find('input').prop("value", origin_namespace)
+  import_button.enable().removeClass('is-loading')
diff --git a/app/views/import/bitbucket/deploy_key.js.haml b/app/views/import/bitbucket/deploy_key.js.haml
new file mode 100644
index 0000000000000000000000000000000000000000..81b34ab5c9df038c9e8ce82efcea4f1c18f18e4b
--- /dev/null
+++ b/app/views/import/bitbucket/deploy_key.js.haml
@@ -0,0 +1,3 @@
+:plain
+  job = $("tr#repo_#{@repo_id}")
+  job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 15dd98077c8736744d64427fbc7bdb0c36ee85f6..f8b4b107513991c0005964c2f416ab545332b49f 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -51,7 +51,7 @@
           %td
             = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
           %td.import-target
-            = "#{repo["owner"]}/#{repo["slug"]}"
+            = import_project_target(repo['owner'], repo['slug'])
           %td.import-actions.job-status
             = button_tag class: "btn btn-import js-add-to-import" do
               Import
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 54ff1d27c67bd1c58057df9ed768aacdb55abe39..4c721d40b55e39666884a4912dbb5f4f919a1dcb 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -45,7 +45,17 @@
           %td
             = github_project_link(repo.full_name)
           %td.import-target
-            = repo.full_name
+            %fieldset.row
+            .input-group
+              .project-path.input-group-btn
+                - if current_user.can_select_namespace?
+                  - selected = params[:namespace_id] || :current_user
+                  - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
+                  = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+                - else
+                  = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+              %span.input-group-addon /
+              = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
           %td.import-actions.job-status
             = button_tag class: "btn btn-import js-add-to-import" do
               Import
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index fcfc6fd37f4e804d07db5bab1389ae5adea1b889..d31fc2e6adb6d2c5b1f35eec687f6c1da63091dd 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -45,7 +45,7 @@
           %td
             = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
           %td.import-target
-            = repo["path_with_namespace"]
+            = import_project_target(repo['namespace']['path'], repo['name'])
           %td.import-actions.job-status
             = button_tag class: "btn btn-import js-add-to-import" do
               Import
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 44e2653ca4affb4c37bba944f977b5d101a97c00..767dffb55891bb12b1ae8fdcb82bc1128abcf8b4 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -9,12 +9,12 @@
   %p
     Project will be imported as
     %strong
-      #{@namespace_name}/#{@path}
+      #{@namespace.name}/#{@path}
 
   %p
     To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
   .form-group
-    = hidden_field_tag :namespace_id, @namespace_id
+    = hidden_field_tag :namespace_id, @namespace.id
     = hidden_field_tag :path, @path
     = label_tag :file, class: 'control-label' do
       %span GitLab project export
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
deleted file mode 100644
index ed3afb0ce3386338fa6359f1991750bbe00b9b9f..0000000000000000000000000000000000000000
--- a/app/views/import/gitorious/status.html.haml
+++ /dev/null
@@ -1,54 +0,0 @@
-- page_title "Gitorious import"
-- header_title "Projects", root_path
-%h3.page-title
-  %i.icon-gitorious.icon-gitorious-big
-  Import projects from Gitorious.org
-
-%p.light
-  Select projects you want to import.
-%hr
-%p
-  = button_tag class: "btn btn-import btn-success js-import-all" do
-    Import all projects
-    = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
-  %table.table.import-jobs
-    %colgroup.import-jobs-from-col
-    %colgroup.import-jobs-to-col
-    %colgroup.import-jobs-status-col
-    %thead
-      %tr
-        %th From Gitorious.org
-        %th To GitLab
-        %th Status
-    %tbody
-      - @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://gitorious.org/#{project.import_source}", target: "_blank"
-          %td
-            = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
-          %td.job-status
-            - if project.import_status == 'finished'
-              %span
-                %i.fa.fa-check
-                done
-            - elsif project.import_status == 'started'
-              %i.fa.fa-spinner.fa-spin
-              started
-            - else
-              = project.human_import_status_name
-
-      - @repos.each do |repo|
-        %tr{id: "repo_#{repo.id}"}
-          %td
-            = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
-          %td.import-target
-            = repo.full_name
-          %td.import-actions.job-status
-            = button_tag class: "btn btn-import js-add-to-import" do
-              Import
-              = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } }
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 80ca30f36e66f10675a6067016fd82e31ed7f3fc..889514c4755fa161d9d26baddd13e4568552741b 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,6 +4,6 @@
 -#    total_pages:   total number of pages
 -#    per_page:      number of items to fetch per page
 -#    remote:        data-remote
-%li{class: "page"}
-  %span.page.gap
+%li
+  %span.gap
     = raw(t 'views.pagination.truncate')
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 522e4d1d05fd57c70fb789f8d9177df38b055e5a..750aed8f329350297d38dd91989b6e2c1a87d322 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
 -#    total_pages:   total number of pages
 -#    per_page:      number of items to fetch per page
 -#    remote:        data-remote
-%li{class: "page#{' active' if page.current?}"}
+%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
   = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..65887aacbafb8ee20c1c6dcc3c60c993a895e623
--- /dev/null
+++ b/app/views/koding/index.html.haml
@@ -0,0 +1,6 @@
+.row-content-block.second-block.center
+  %p
+    = icon('circle', class: 'cgreen')
+    Integration is active for
+    = link_to koding_project_url, target: '_blank' do
+      #{current_application_settings.koding_url}
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 3612f1ce5c693d67df409294db63e50478f2457b..baa8036de10f1be1e86e606f03ca10b7484d7e76 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
 .flash-container.flash-container-page
   - if alert
     .flash-alert
-      = alert
+      %div{ class: (container_class) }
+        %span= alert
 
   - elsif notice
     .flash-notice
-      = notice
+      %div{ class: (container_class) }
+        %span= notice
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 351100f3523383a3a723f261f6ab5fe96b6fcab0..e138ebab0188767b60b2313c3883ea30ee26a4f2 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,7 +1,8 @@
 - project = @target_project || @project
-- noteable_class = @noteable.class if @noteable.present?
+- noteable_type = @noteable.class if @noteable.present?
 
-:javascript
-  GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_class, type_id: params[:id])}"
-  GitLab.GfmAutoComplete.cachedData = undefined;
-  GitLab.GfmAutoComplete.setup();
+- if project
+  :javascript
+    GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+    GitLab.GfmAutoComplete.cachedData = undefined;
+    GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a1a71c2fb3392929b669075ece43dc6174168d51..8aefdcb3d9b10e25505ec06cad583b596fae37ea 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,10 +1,11 @@
 .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
-  .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
+  .sidebar-wrapper.nicescroll
     .sidebar-action-buttons
-      = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
+      .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
         %span.sr-only Toggle navigation
         = icon('bars')
-      = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
+
+      %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
         %span.sr-only Toggle navigation pinning
         = icon('fw thumb-tack')
 
@@ -20,10 +21,10 @@
       .container-fluid
         = render "layouts/nav/#{nav}"
   .content-wrapper{ class: "#{layout_nav_class}" }
+    = yield :sub_nav
     = render "layouts/broadcast"
     = render "layouts/flash"
     = yield :flash_message
-    %div{ class: (container_class unless @no_container) }
+    %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
       .content
-        .clearfix
-          = yield
+        = yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index f7580f00159582c705f2b70fdf04c4ffa832081b..8e65bd12c569befa909dd63a78270f10b30f6953 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,15 +2,18 @@
   - label = 'This group'
 - if controller.controller_path =~ /^projects/ && @project.persisted?
   - label = 'This project'
-
+- if @group && @group.persisted? && @group.path
+  - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
+- if @project && @project.persisted?
+  - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
 .search.search-form{class: "#{'has-location-badge' if label.present?}"}
   = form_tag search_path, method: :get, class: 'navbar-form' do |f|
     .search-input-container
       - if label.present?
         .location-badge= label
       .search-input-wrap
-        .dropdown{ data: {url: search_autocomplete_path } }
-          = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off',  data: { toggle: 'dropdown' }
+        .dropdown{ data: { url: search_autocomplete_path } }
+          = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
           .dropdown-menu.dropdown-select
             = dropdown_content do
               %ul
@@ -21,8 +24,9 @@
           %i.search-icon
           %i.clear-icon.js-clear-input
 
-    = hidden_field_tag :group_id, @group.try(:id)
-    = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+    = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
+
+    = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs
 
     - if @project && @project.persisted?
       - if current_controller?(:issues)
@@ -36,31 +40,6 @@
       - else
         = hidden_field_tag :search_code, true
 
-      :javascript
-        gl.projectOptions = gl.projectOptions || {};
-        gl.projectOptions["#{j(@project.path)}"] = {
-          issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
-          mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
-          name: "#{j(@project.name)}"
-        };
-
-    - if @group && @group.persisted? && @group.path
-      :javascript
-        gl.groupOptions = gl.groupOptions || {};
-        gl.groupOptions["#{j(@group.path)}"] = {
-          name: "#{j(@group.name)}",
-          issuesPath: "#{issues_group_path(j(@group.path))}",
-          mrPath: "#{merge_requests_group_path(j(@group.path))}"
-        };
-
-
-    :javascript
-      gl.dashboardOptions = {
-        issuesPath: "#{issues_dashboard_url}",
-        mrPath: "#{merge_requests_dashboard_url}"
-      };
-
-
     - if @snippet || @snippets
       = hidden_field_tag :snippets, true
     = hidden_field_tag :repository_ref, @ref
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 33cedaaf2eee64e17ae15780b35309b275cb9f83..6c2285fa2b6322fe1b5d241521aef3cd757deccb 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,5 +1,5 @@
 !!! 5
-%html{ lang: "en"}
+%html{ lang: "en", class: "#{page_class}" }
   = render "layouts/head"
   %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
     = Gon::Base.render_data
@@ -11,3 +11,4 @@
     = render 'layouts/page', sidebar: sidebar, nav: nav
 
     = yield :scripts_body
+    = render "layouts/init_auto_complete" if @gfm_form
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 3d28eec84ef584c33b424cc73acd329550aa811a..afd9958f0738effa3c2a7a90d9609dea5f967f33 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,36 +1,37 @@
 !!! 5
-%html{ lang: "en"}
+%html{ lang: "en", class: "devise-layout-html"}
   = render "layouts/head"
-  %body.ui_charcoal.login-page.application.navless
-    = Gon::Base.render_data
-    = render "layouts/header/empty"
-    = render "layouts/broadcast"
-    .container.navless-container
-      .content
-        = render "layouts/flash"
-        .row
-          .col-sm-5.pull-right
-            = yield
-          .col-sm-7.brand-holder.pull-left
-            %h1
-              = brand_title
-            - if brand_item
-              = brand_image
-              = brand_text
-            - else
-              %h3 Open source software to collaborate on code
+  %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }}
+    .page-wrap
+      = Gon::Base.render_data
+      = render "layouts/header/empty"
+      = render "layouts/broadcast"
+      .container.navless-container
+        .content
+          = render "layouts/flash"
+          .row
+            .col-sm-5.pull-right.new-session-forms-container
+              = yield
+            .col-sm-7.brand-holder.pull-left
+              %h1
+                = brand_title
+              - if brand_item
+                = brand_image
+                = brand_text
+              - else
+                %h3 Open source software to collaborate on code
 
-              %p
-                Manage git repositories with fine grained access controls that keep your code secure.
-                Perform code reviews and enhance collaboration with merge requests.
-                Each project can also have an issue tracker and a wiki.
+                %p
+                  Manage Git repositories with fine-grained access controls that keep your code secure.
+                  Perform code reviews and enhance collaboration with merge requests.
+                  Each project can also have an issue tracker and a wiki.
 
-            - if extra_sign_in_text.present?
-              = markdown(extra_sign_in_text)
+              - if current_application_settings.sign_in_text.present?
+                = markdown_field(current_application_settings, :sign_in_text)
 
-    %hr
-    .container
-      .footer-links
-        = link_to "Explore", explore_root_path
-        = link_to "Help", help_path
-        = link_to "About GitLab", "https://about.gitlab.com/"
+      %hr.footer-fixed
+      .container.footer-container
+        .footer-links
+          = link_to "Explore", explore_root_path
+          = link_to "Help", help_path
+          = link_to "About GitLab", "https://about.gitlab.com/"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 94c53882623c09f68f7d31327cfd23cbb540e014..7a9859262f7f5a8f79c7054940d52b1f6bf9e2f4 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,5 +1,5 @@
 %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
-  %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
+  %div{ class: "container-fluid" }
     .header-content
       %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
         %span.sr-only Toggle navigation
@@ -29,10 +29,6 @@
                 = icon('bell fw')
                 %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
                   = 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',
@@ -41,13 +37,15 @@
             %li.header-user.dropdown
               = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
                 = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
-                %span.caret
+                = icon('caret-down')
               .dropdown-menu-nav.dropdown-menu-align-right
                 %ul
                   %li
                     = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
                   %li
                     = link_to "Profile Settings", profile_path, aria: { label: "Profile 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" }
diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..22319bba745f84a8c597eab946aca8a455c2a1f9
--- /dev/null
+++ b/app/views/layouts/koding.html.haml
@@ -0,0 +1,5 @@
+- page_title        "Koding"
+- page_description  "Koding Dashboard"
+- header_title      "Koding", koding_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 3a14751ea8ebd9708f62eb5064d3c6eaab5b4938..a0356feef95b401d103301660c0767e6dea8e45a 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,44 +1,38 @@
-%ul.nav.nav-sidebar
-  = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
-    = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
-      %span
-        Projects
-  = nav_link(controller: :todos) do
-    = link_to dashboard_todos_path, title: 'Todos' do
-      %span
-        Todos
-        %span.count= number_with_delimiter(todos_pending_count)
-  = nav_link(path: 'dashboard#activity') do
-    = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
-      %span
-        Activity
-  = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
-    = link_to dashboard_groups_path, title: 'Groups' do
-      %span
-        Groups
-  = nav_link(controller: 'dashboard/milestones') do
-    = link_to dashboard_milestones_path, title: 'Milestones' do
-      %span
-        Milestones
-  = nav_link(path: 'dashboard#issues') do
-    = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
-      %span
-        Issues
-        %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
-  = 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
-        %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
-  = nav_link(controller: 'dashboard/snippets') do
-    = link_to dashboard_snippets_path, title: 'Snippets' do
-      %span
-        Snippets
-  = nav_link(controller: :help) do
-    = link_to help_path, title: 'Help' do
-      %span
-        Help
-  = nav_link(html_options: {class: profile_tab_class}) do
-    = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
-      %span
-        Profile Settings
+.nav-sidebar
+  .sidebar-header Across GitLab
+  %ul.nav
+    = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+      = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+        %span
+          Projects
+    = nav_link(path: 'dashboard#activity') do
+      = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+        %span
+          Activity
+    - if koding_enabled?
+      = nav_link(controller: :koding) do
+        = link_to koding_path, title: 'Koding' do
+          %span
+            Koding
+    = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+      = link_to dashboard_groups_path, title: 'Groups' do
+        %span
+          Groups
+    = nav_link(controller: 'dashboard/milestones') do
+      = link_to dashboard_milestones_path, title: 'Milestones' do
+        %span
+          Milestones
+    = nav_link(path: 'dashboard#issues') do
+      = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+        %span
+          Issues
+          %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+    = 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
+          %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+    = nav_link(controller: 'dashboard/snippets') do
+      = link_to dashboard_snippets_path, title: 'Snippets' do
+        %span
+          Snippets
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index d7d36c84b6caa9dc647e823af93cb427f2f1ea9d..f7edb47b6663d15d306f6d914a51fedf7573d58b 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,5 +1,5 @@
+= render 'layouts/nav/group_settings'
 .scrolling-tabs-container{ class: nav_control_class }
-  = render 'layouts/nav/group_settings'
   .fade-left
     = icon('angle-left')
   .fade-right
@@ -13,6 +13,10 @@
       = 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
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index bf9a7ecb78679cfa46ee9708bc38f91d71ee6f0d..75275afc0f3058f85d0bbb0bbb1c07bfde805b1e 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,22 +1,26 @@
 - if current_user
+  - can_admin_group = can?(current_user, :admin_group, @group)
   - can_edit = can?(current_user, :admin_group, @group)
   - member = @group.members.find_by(user_id: current_user.id)
   - can_leave = member && can?(current_user, :destroy_group_member, member)
 
-  .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
-        = nav_link(path: 'groups#projects') do
-          = link_to 'Projects', projects_group_path(@group), title: 'Projects'
-        %li.divider
-        - if can_edit
-          %li
-            = link_to 'Edit Group', edit_group_path(@group)
-        - if can_leave
-          %li
-            = link_to polymorphic_path([:leave, @group, :members]),
-              data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
-              Leave Group
+  - if can_admin_group || can_edit || can_leave
+    .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_leave
+            %li.divider
+          - if can_edit
+            %li
+              = link_to 'Edit Group', edit_group_path(@group)
+          - if can_leave
+            %li
+              = link_to polymorphic_path([:leave, @group, :members]),
+                data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
+                Leave Group
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 1d3b8fc36833a3dc198c3d52f7d294dece871c10..99a58bbb676501cd92b6656723ae8ec6da8aaabd 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -47,7 +47,7 @@
             Repository
 
     - if project_nav_tab? :pipelines
-      = nav_link(controller: [:pipelines, :builds, :environments]) do
+      = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
         = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
           %span
             Pipelines
@@ -65,7 +65,7 @@
             Graphs
 
     - if project_nav_tab? :issues
-      = nav_link(controller: [:issues, :labels, :milestones]) do
+      = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
         = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
           %span
             Issues
@@ -113,3 +113,7 @@
       %li.hidden
         = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
           Commits
+
+    -# Shortcut to issue boards
+    %li.hidden
+      = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 52a5bdc1a1b1093f98ed10ff6bf7f42f51c24d6d..613b8b7d3013d1b3c30e5156ef20a0f519e9898f 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -26,7 +26,7 @@
       %span
         Protected Branches
 
-  - if @project.builds_enabled?
+  - if @project.feature_available?(:builds, current_user)
     = nav_link(controller: :runners) do
       = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
         %span
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index dde2e2889dc31cdd86da0ec57e9db3006ce3edc1..1ec4c3f0c6730222f37263cb1dbc457f8b644e2e 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -25,8 +25,8 @@
           - if @labels_url
             adjust your #{link_to 'label subscriptions', @labels_url}.
           - else
-            - if @sent_notification && @sent_notification.unsubscribable?
-              = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+            - if @sent_notification_url
+              = link_to "unsubscribe", @sent_notification_url
               from this thread or
             adjust your notification settings.
 
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 9fe94291db749dfa5c8c337213dfa04339678daa..277eb71ea739e04c968590b4305e3992a64d47e6 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,9 +14,6 @@
       window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
       window.preview_markdown_path = "#{preview_markdown_path}";
 
-- content_for :scripts_body do
-  = render "layouts/init_auto_complete" if current_user
-
 - content_for :header_content do
   .js-dropdown-menu-projects
     .dropdown-menu.dropdown-select.dropdown-menu-projects
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4f3d36bd9cada54e93286fab68177e0953ac7060
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -0,0 +1,12 @@
+%p
+  You have been mentioned in an issue.
+
+- if current_application_settings.email_author_in_body
+  %div
+    #{link_to @issue.author_name, user_url(@issue.author)} wrote:
+-if @issue.description
+  = markdown(@issue.description, pipeline: :email, author: @issue.author)
+
+- if @issue.assignee_id.present?
+  %p
+    Assignee: #{@issue.assignee_name}
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..457e94b48001b3fd85b39d1095e2a00f6b4935ec
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -0,0 +1,7 @@
+You have been mentioned in an issue.
+
+Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
+Author:    <%= @issue.author_name %>
+Assignee:  <%= @issue.assignee_name %>
+
+<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..32aedb9e6b91a3259be0c1a6af41b370888569ed
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -0,0 +1,15 @@
+%p
+  You have been mentioned in Merge Request #{@merge_request.to_reference}
+
+- if current_application_settings.email_author_in_body
+  %div
+    #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
+%p.details
+  != merge_path_description(@merge_request, '&rarr;')
+
+- if @merge_request.assignee_id.present?
+  %p
+    Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
+
+-if @merge_request.description
+  = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..5bf0282e0974e41d55f2626b3564ef2089fa7474
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -0,0 +1,9 @@
+You have been mentioned in Merge Request <%= @merge_request.to_reference %>
+
+<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
+
+<%= merge_path_description(@merge_request, 'to') %>
+Author:    <%= @merge_request.author_name %>
+Assignee:  <%= @merge_request.assignee_name %>
+
+<%= @merge_request.description %>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..38c852f0a3a1f4b0bd7aa7d6d8dffcd0709a5f00
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -0,0 +1,177 @@
+<!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;"}
+            %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;"}
+              %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"}/
+                                          %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"}/
+                                          %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"}/
+                                          %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;"}
+                                      %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"}
+                                        = build.name
+                                  %tr.build-log
+                                    %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
+        %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
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..8f8084b58e1ce3cb56248fe462206a48ff195537
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -0,0 +1,31 @@
+Your pipeline has failed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% failed = @pipeline.statuses.latest.failed -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+Stage: <%= build.stage %>
+Name: <%= build.name %>
+Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+
+<% 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
new file mode 100644
index 0000000000000000000000000000000000000000..697c8d19257cf79dd54bfb9e53617dc84248c376
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -0,0 +1,154 @@
+<!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;"}
+            %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;"}
+              %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"}/
+                                          %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"}/
+                                          %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"}/
+                                          %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.size
+                            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
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..ae22d474f2ca32fefb182670617f8ee8731233b1
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -0,0 +1,24 @@
+Your pipeline has passed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% build_count = @pipeline.statuses.latest.size -%>
+<% stage_count = @pipeline.stages.size -%>
+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/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c161ecc3463a262fcca6dee0e5e0637b48b2d04c..c0c07d65daa49a256f8235057ccd714bb7270e66 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -75,8 +75,7 @@
             - blob = diff_file.blob
             - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
               %table.code.white
-                - diff_file.highlighted_diff_lines.each do |line|
-                  = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
+                = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
             - else
               No preview for this file type
           %br
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..522421b7cc351d449faa60d0fbd91dea1cce9930
--- /dev/null
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -0,0 +1,2 @@
+%p
+  All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name}
diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..b0d380af8fcb07fa745080c709c53856ec1b7587
--- /dev/null
+++ b/app/views/notify/resolved_all_discussions_email.text.erb
@@ -0,0 +1,3 @@
+All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %>
+
+<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index c80f22457b4eb8d673a7e39d46010bb86b7341f8..72f658d1b68d70756e5438a69e222d2489db3f32 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -8,24 +8,36 @@
 .row.prepend-top-default
   .col-lg-3.profile-settings-sidebar
     %h4.prepend-top-0
-      Private Token
+      = incoming_email_token_enabled? ? "Private Tokens" : "Private Token"
     %p
-      Your private token is used to access application resources without authentication.
-  .col-lg-9
-    = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
+      Keep
+      = incoming_email_token_enabled? ? "these tokens" : "this token"
+      secret, anyone with access to them can interact with GitLab as if they were you.
+  .col-lg-9.private-tokens-reset
+    .reset-action
       %p.cgray
         - if current_user.private_token
-          = label_tag "token", "Private token", class: "label-light"
-          = text_field_tag "token", current_user.private_token, class: "form-control"
+          = label_tag "private-token", "Private token", class: "label-light"
+          = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()"
         - else
-          %span You don`t have one yet. Click generate to fix it.
-      %p.help-block
-        It can be used for atom feeds or the API. Keep it secret!
+          %span You don't have one yet. Click generate to fix it.
+        %p.help-block
+          Your private token is used to access the API and Atom feeds without username/password authentication.
       .prepend-top-default
         - if current_user.private_token
-          = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+          = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token"
         - else
           = f.submit 'Generate', class: "btn btn-default"
+    - if incoming_email_token_enabled?
+      .reset-action
+        %p.cgray
+          = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light'
+          = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()"
+        %p.help-block
+          Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.
+        .prepend-top-default
+          = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token"
+
 %hr
 .row.prepend-top-default
   .col-lg-3.profile-settings-sidebar
@@ -86,11 +98,11 @@
           = f.label :username, "Path", class: "label-light"
           .input-group
             .input-group-addon
-              = "#{root_url}u/"
+              = root_url
             = f.text_field :username, required: true, class: 'form-control'
         .help-block
           Current path:
-          = "#{root_url}u/#{current_user.username}"
+          = "#{root_url}#{current_user.username}"
         .prepend-top-default
           = f.button class: "btn btn-warning", type: "submit" do
             = icon "spinner spin", class: "hidden loading-username"
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index a42b3b8eb3830cea042ab454a3d0e0c06ad826d7..931878735014e0e341c9f8448c641d5accd60cd8 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,4 +1,5 @@
 - page_title "SSH Keys"
+= render 'profiles/head'
 
 .row.prepend-top-default
   .col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 71ac367830d7a913e1abd36c4a3b3830e330d6ef..05a2ea67aa2189c8327fe45aa409c21f70ced497 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -7,6 +7,10 @@
       = page_title
     %p
       You can generate a personal access token for each application you use that needs access to the GitLab API.
+    %p
+      You can also use personal access tokens to authenticate against Git over HTTP.
+      They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
+
   .col-lg-9
 
     - if flash[:personal_access_token]
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 4433cab7782f5ea3a0be4e3f386da29562cd675c..8966dd3fd862fe8bc3dd4c4c20a6fcb84375fa86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
 
 // Toggle container-fluid class
 if ('<%= current_user.layout %>' === 'fluid') {
-  $('.content-wrapper').find('.container-fluid').removeClass('container-limited')
+  $('.content-wrapper .container-fluid').removeClass('container-limited')
 } else {
-  $('.content-wrapper').find('.container-fluid').addClass('container-limited')
+  $('.content-wrapper .container-fluid').addClass('container-limited')
 }
 
 // Re-enable the "Save" button
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d9fa74fad906fd77d6b5e9171b0ed4e0b8d743fc..578af9fe98dd6c29db50201338dc34159a52f540 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -86,6 +86,9 @@
       .form-group
         = f.label :location, 'Location', class: "label-light"
         = f.text_field :location, class: "form-control"
+      .form-group
+        = f.label :organization, 'Organization', class: "label-light"
+        = f.text_field :organization, class: "form-control"
       .form-group
         = f.label :bio, class: "label-light"
         = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 366f1fed35bb44e15eb1d662d1fa0eaed4cfa39d..03ac739ade51517de77757c31335e0f6445d2918 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -60,13 +60,38 @@
       two-factor authentication app before a U2F device. That way you'll always be able to
       log in - even when you're using an unsupported browser.
   .col-lg-9
-    %p
-      - if @registration_key_handles.present?
-        = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
     - if @u2f_registration.errors.present?
       = form_errors(@u2f_registration)
     = render "u2f/register"
 
+    %hr
+
+    %h5 U2F Devices (#{@u2f_registrations.length})
+
+    - if @u2f_registrations.present?
+      .table-responsive
+        %table.table.table-bordered.u2f-registrations
+          %colgroup
+            %col{ width: "50%" }
+            %col{ width: "30%" }
+            %col{ width: "20%" }
+          %thead
+            %tr
+              %th Name
+              %th Registered On
+              %th
+          %tbody
+            - @u2f_registrations.each do |registration|
+              %tr
+                %td= registration.name.presence || "<no name set>"
+                %td= registration.created_at.to_date.to_s(:medium)
+                %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+
+    - else
+      .settings-message.text-center
+        You don't have any U2F devices registered yet.
+
+
 - if two_factor_skippable?
   :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>";
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
index 249680bcab6634f7dfd9d7806058053a1a91c9c9..de1337a2a24e093ff7a4a26244c0c8d7c28a53f7 100644
--- a/app/views/profiles/update_username.js.haml
+++ b/app/views/profiles/update_username.js.haml
@@ -1,6 +1,6 @@
 - if @user.valid?
   :plain
-    new Flash("Username sucessfully changed", "notice")
+    new Flash("Username successfully changed", "notice")
 - else
   :plain
     new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index ac50ce83f6afcf9eb012ab1a2f1930901854d64f..d011e51e6968c7f6162e7ecbbf79d05caeb53e71 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,13 +1,16 @@
-.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
-        %i.fa.fa-rss
+- @no_container = true
 
-  = render 'shared/event_filter'
+%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')
 
-.content_list.project-activity{:"data-href" => activity_project_path(@project)}
-= spinner
+    = render 'shared/event_filter'
+
+  .content_list.project-activity{:"data-href" => activity_project_path(@project)}
+  = spinner
 
 :javascript
   var activity = new Activities();
diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d2c1e943db11911cadf1b525e8971e652b4e5273
--- /dev/null
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -0,0 +1,8 @@
+.row-content-block.project-home-empty
+  %div.text-center{ class: container_class }
+    %h4
+      Customize your workflow!
+    %p
+      Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
+    - if can?(current_user, :admin_project, @project)
+      = link_to "Get started", edit_project_path(@project), class: "btn btn-success"
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8ef31ca3bda74bba0c4ec63d8bdb3fafdc0125dd..5a04c3318cfabb806947cde2a6edbed60ff6bfd5 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,8 @@
 - empty_repo = @project.empty_repo?
 .project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
   %div{ class: container_class }
-    = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile')
+    .avatar-container.s70.project-avatar
+      = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
     %h1.project-title
       = @project.name
       %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
@@ -9,7 +10,7 @@
 
     .project-home-desc
       - if @project.description.present?
-        = markdown(@project.description, pipeline: :description)
+        = markdown_field(@project, :description)
 
       - if forked_from_project = @project.forked_from_project
         %p
@@ -22,5 +23,6 @@
         = render 'projects/buttons/star'
         = render 'projects/buttons/fork'
 
-      .project-clone-holder
-        = render "shared/clone_panel"
+      - if @project.feature_available?(:repository, current_user)
+        .project-clone-holder
+          = render "shared/clone_panel"
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 630ae7d61405c64aabc3a3927ef4c2114379251d..7f530708947084bf3851be26809d252c9548c102 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,10 +1,12 @@
-- if commit.status
-  = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do
-    = ci_icon_for_status(commit.status)
-    = ci_label_for_status(commit.status)
+- ref = local_assigns.fetch(:ref)
+- status = commit.status(ref)
+- if status
+  = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
+    = ci_icon_for_status(status)
+    = ci_label_for_status(status)
 
 = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
 = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
 &middot;
-#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+#{time_ago_with_tooltip(commit.committed_date)} by
 = commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 3c6b931f41aea6092b305fb4922304270cf97fa9..1c3bccccb5cb20e41db584589337e34ae544c103 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,6 +1,6 @@
 - if event = last_push_event
   - if show_last_push_widget?(event)
-    .row-content-block.top-block.clear-block.hidden-xs
+    .row-content-block.top-block.hidden-xs.white
       %div{ class: container_class }
         .event-last-push
           .event-last-push-text
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 19b4249374b2d65caca94a15925e8258b6e6f33a..6e143c4b570e526e70c5663080f9b036cae05792 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,11 +1,18 @@
-%fieldset.builds-feature
-  %h5.prepend-top-0
-    Merge Requests
-  .form-group
-    .checkbox
-      = f.label :only_allow_merge_if_build_succeeds do
-        = f.check_box :only_allow_merge_if_build_succeeds
-        %strong Only allow merge requests to be merged if the build succeeds
-      .help-block
-        Builds need to be configured to enable this feature.
-        = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+.merge-requests-feature
+  %fieldset.builds-feature
+    %hr
+    %h5.prepend-top-0
+      Merge Requests
+    .form-group
+      .checkbox
+        = f.label :only_allow_merge_if_build_succeeds do
+          = f.check_box :only_allow_merge_if_build_succeeds
+          %strong Only allow merge requests to be merged if the build succeeds
+          %br
+          %span.descr
+            Builds need to be configured to enable this feature.
+            = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+      .checkbox
+        = f.label :only_allow_merge_if_all_discussions_are_resolved do
+          = f.check_box :only_allow_merge_if_all_discussions_are_resolved
+          %strong Only allow merge requests to be merged if all discussions are resolved
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f00422dd7c08846400f874156de7bc9c5765045b
--- /dev/null
+++ b/app/views/projects/_wiki.html.haml
@@ -0,0 +1,19 @@
+- if @wiki_home.present?
+  %div{ class: container_class }
+    .wiki-holder.prepend-top-default.append-bottom-default
+      .wiki
+        = preserve do
+          = render_wiki_content(@wiki_home)
+- else
+  - can_create_wiki = can?(current_user, :create_wiki, @project)
+  .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
+    %div.text-center{ class: container_class }
+      %h4
+        This project does not have a wiki homepage yet
+      - if can_create_wiki
+        %p
+          Add a homepage to your wiki that contains information about your project
+        %p
+          We recommend you
+          = link_to "add a homepage", namespace_project_wiki_path(@project.namespace, @project, :home)
+          to your project's wiki and GitLab will show it here instead of this message.
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 413477a2d3a0082b846aeec6f4b8d330d1f6f1d5..0c8241053e743d8e36247c937d06c6c6968587b3 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,7 +1,9 @@
+- @gfm_form = true
+- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
 .zen-backdrop
   - classes << ' js-gfm-input js-autosize markdown-area'
   - if defined?(f) && f
-    = f.text_area attr, class: classes, placeholder: placeholder
+    = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
   - else
     = text_area_tag attr, nil, class: classes, placeholder: placeholder
   %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 377665b096f5885d620f2450d3089000db12e101..cadfe5a3e30def2d34d0444aba1e2af84a1d4861 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,44 +1,48 @@
+- @no_container = true
 - page_title "Blame", @blob.path, @ref
+= render "projects/commits/head"
 
-%h3.page-title Blame view
+%div{ class: container_class }
+  %h3.page-title Blame view
 
-#blob-content-holder.tree-holder
-  .file-holder
-    .file-title
-      = blob_icon @blob.mode, @blob.name
-      %strong
-        = @path
-      %small= number_to_human_size @blob.size
-      .file-actions
-        = render "projects/blob/actions"
-    .file-content.blame.code.js-syntax-highlight
-      %table
-        - current_line = 1
-        - @blame_groups.each do |blame_group|
-          %tr
-            %td.blame-commit
-              .commit
-                - commit = blame_group[:commit]
-                .commit-row-title
-                  %strong
-                    = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
-                  .pull-right
-                    = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace"
-                  &nbsp;
-                .light
-                  = commit_author_link(commit, avatar: false)
-                  authored
-                  #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
-            %td.line-numbers
-              - line_count = blame_group[:lines].count
-              - (current_line...(current_line + line_count)).each do |i|
-                %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
-                  = icon("link")
-                  = i
-                \
-              - current_line += line_count
-            %td.lines
-              %pre.code.highlight
-                %code
-                  - blame_group[:lines].each do |line|
-                    #{line}
+  #blob-content-holder.tree-holder
+    .file-holder
+      .file-title
+        = blob_icon @blob.mode, @blob.name
+        %strong
+          = @path
+        %small= number_to_human_size @blob.size
+        .file-actions
+          = render "projects/blob/actions"
+      .table-responsive.file-content.blame.code.js-syntax-highlight
+        %table
+          - current_line = 1
+          - @blame_groups.each do |blame_group|
+            %tr
+              %td.blame-commit
+                .commit
+                  - commit = blame_group[:commit]
+                  = author_avatar(commit, size: 36)
+                  .commit-row-title
+                    %strong
+                      = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
+                    .pull-right
+                      = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace"
+                    &nbsp;
+                  .light
+                    = commit_author_link(commit, avatar: false)
+                    authored
+                    #{time_ago_with_tooltip(commit.committed_date)}
+              %td.line-numbers
+                - line_count = blame_group[:lines].count
+                - (current_line...(current_line + line_count)).each do |i|
+                  %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+                    = icon("link")
+                    = i
+                  \
+                - current_line += line_count
+              %td.lines
+                %pre.code.highlight
+                  %code
+                    - blame_group[:lines].each do |line|
+                      #{line}
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 3ffc3fcb7ac3e2c91ed6efba29b1ef16ba221076..149ee7c59d6a4b3625408c3d9111d8c173b84f15 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -20,7 +20,7 @@
 
 %ul.blob-commit-info.hidden-xs
   - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
-  = render blob_commit, project: @project
+  = render blob_commit, project: @project, ref: @ref
 
 %div#blob-content-holder.blob-content-holder
   %article.file-holder
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 0237e152b54b577f27035b4d40b444fd4ff521c3..4a6aa92e3f3db04685091e0882c416a59e2a3ac2 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -14,13 +14,20 @@
       = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
         required: true, class: 'form-control new-file-name'
 
-    .pull-right
+    .pull-right.file-buttons
       .license-selector.js-license-selector-wrap.hidden
-        = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+        = dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
       .gitignore-selector.js-gitignore-selector-wrap.hidden
-        = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
+        = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
       .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
-        = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+        = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+      = button_tag class: 'soft-wrap-toggle btn', type: 'button' do
+        %span.no-wrap
+          = custom_icon('icon_no_wrap')
+          No wrap
+        %span.soft-wrap
+          = custom_icon('icon_soft_wrap')
+          Soft wrap
       .encoding-selector
         = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
 
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index b1f50eb5f34ba50d771bbab69945e209137915c6..57a27ec904e4148b9e8159088340990af9ae7e35 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -26,6 +26,6 @@
 
 
 :javascript
-  disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
+  gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
   new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
   new NewCommitForm($('.js-upload-blob-form'))
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7b0621f94013c52a50c052e0fa176754ce48f9ab..2a0352a71b79a19a3acfa49292061756b13e1db5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,29 +1,31 @@
+- @no_container = true
 - page_title "Edit", @blob.path, @ref
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('lib/ace.js')
+  = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+= render "projects/commits/head"
 
-- 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"
-    and make sure your changes will not unintentionally remove theirs.
+%div{ class: container_class }
+  - 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"
+      and make sure your changes will not unintentionally remove theirs.
 
-.file-editor
-  %ul.nav-links.no-bottom.js-edit-mode
-    %li.active
-      = link_to '#editor' do
-        Edit File
+  .file-editor
+    %ul.nav-links.no-bottom.js-edit-mode
+      %li.active
+        = link_to '#editor' do
+          Edit File
 
-    %li
-      = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
-        = editing_preview_title(@blob.name)
+      %li
+        = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
+          = editing_preview_title(@blob.name)
 
-  = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
-    = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
-    = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
-    = hidden_field_tag 'last_commit_sha', @last_commit_sha
-    = hidden_field_tag 'content', '', id: "file-content"
-    = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
-    = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
-
-:javascript
-  blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
-  new NewCommitForm($('.js-edit-blob-form'))
+    = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
+      = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
+      = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
+      = hidden_field_tag 'last_commit_sha', @last_commit_sha
+      = hidden_field_tag 'content', '', id: "file-content"
+      = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
+      = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index c952bc7e5dbcf73de34502ab225769e10ffbd0d2..b6ed9518c489a353a8727de73ff1e2918eff6d33 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,17 +1,16 @@
 - page_title "New File", @path.presence, @ref
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('lib/ace.js')
+  = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
 
 %h3.page-title
   New File
 
 .file-editor
-  = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
+  = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
     = render 'projects/blob/editor', ref: @ref
     = render 'shared/new_commit_form', placeholder: "Add new file"
 
     = hidden_field_tag 'content', '', id: 'file-content'
     = render 'projects/commit_button', ref: @ref,
               cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
-
-:javascript
-  blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
-  new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..97eb952eff1dffb00c201ca88010fcf75cfe5fcf
--- /dev/null
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -0,0 +1,15 @@
+%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
new file mode 100644
index 0000000000000000000000000000000000000000..f7071051efca868b014800ff912f63e53522f8c1
--- /dev/null
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -0,0 +1,80 @@
+%board{ "inline-template" => true,
+  "v-cloak" => true,
+  "v-for" => "list in state.lists | orderBy 'position'",
+  "v-ref:board" => true,
+  ":list" => "list",
+  ":disabled" => "disabled",
+  ":issue-link-base" => "issueLinkBase",
+  "track-by" => "_uid" }
+  .board{ ":class" => "{ 'is-draggable': !list.preset }",
+    ":data-id" => "list.id" }
+    .board-inner
+      %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
+        %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
+          %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
+            data: { container: "body", placement: "bottom" } }
+            {{ list.title }}
+          .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
+            %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
+              {{ list.issuesSize }}
+            - if can?(current_user, :admin_issue, @project)
+              %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+                "@click" => "showNewIssueForm",
+                "v-if" => "list.type !== 'done'",
+                "aria-label" => "Add an issue",
+                "title" => "Add an issue",
+                data: { placement: "top", container: "body" } }
+                = icon("plus")
+          - if can?(current_user, :admin_list, @project)
+            %board-delete{ "inline-template" => true,
+              ":list" => "list",
+              "v-if" => "!list.preset && list.id" }
+              %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+                = icon("trash")
+      %board-list{ "inline-template" => true,
+        "v-if" => "list.type !== 'blank'",
+        ":list" => "list",
+        ":issues" => "list.issues",
+        ":loading" => "list.loading",
+        ":disabled" => "disabled",
+        ":show-issue-form.sync" => "showIssueForm",
+        ":issue-link-base" => "issueLinkBase" }
+        .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",
+            ":show-issue-form.sync" => "showIssueForm",
+            "v-show" => "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",
+                  "v-el:input" => true,
+                  ":id" => "list.id + '-title'" }
+                .clearfix.prepend-top-10
+                  %button.btn.btn-success.pull-left{ type: "submit",
+                    ":disabled" => "title === ''",
+                    "v-el:submit-button" => true }
+                    Submit issue
+                  %button.btn.btn-default.pull-right{ type: "button",
+                    "@click" => "cancel" }
+                    Cancel
+        %ul.board-list{ "v-el:list" => true,
+          "v-show" => "!loading",
+          ":data-board" => "list.id",
+          ":class" => "{ 'is-smaller': showIssueForm }" }
+          = render "projects/boards/components/card"
+          %li.board-list-count.text-center{ "v-if" => "showCount" }
+            = icon("spinner spin", "v-show" => "list.loadingMore" )
+            %span{ "v-if" => "list.issues.length === list.issuesSize" }
+              Showing all issues
+            %span{ "v-else" => true }
+              Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
+      - if can?(current_user, :admin_list, @project)
+        = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..8fce702314c3b8d3d7f226b1f55fc29395f17388
--- /dev/null
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -0,0 +1,36 @@
+%board-card{ "inline-template" => true,
+  "v-for" => "issue in issues | orderBy 'priority'",
+  "v-ref:issue" => true,
+  ":index" => "$index",
+  ":list" => "list",
+  ":issue" => "issue",
+  ":issue-link-base" => "issueLinkBase",
+  ":disabled" => "disabled",
+  "track-by" => "id" }
+  %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
+    ":index" => "index",
+    "@mousedown" => "mouseDown",
+    "@mouseMove" => "mouseMove",
+    "@mouseup" => "showIssue($event)" }
+    %h4.card-title
+      = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
+      %a{ ":href" => "issueLinkBase + '/' + issue.id",
+        ":title" => "issue.title" }
+        {{ issue.title }}
+    .card-footer
+      %span.card-number{ "v-if" => "issue.id" }
+        = precede '#' do
+          {{ issue.id }}
+      %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
+        ":title" => "'Assigned to ' + issue.assignee.name",
+        "v-if" => "issue.assignee",
+        data: { container: 'body' } }
+        %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
+      %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
+        type: "button",
+        "v-if" => "(!list.label || label.id !== list.label.id)",
+        "@click" => "filterByLabel(label, $event)",
+        ":style" => "{ backgroundColor: label.color, color: label.textColor }",
+        ":title" => "label.description",
+        data: { container: 'body' } }
+        {{ label.title }}
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f0c0c6953e04d59acb1102d40fb95d1fb9b64ee0
--- /dev/null
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%board-sidebar{ "inline-template" => true,
+  ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+  %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+    .issuable-sidebar
+      .block.issuable-sidebar-header
+        %span.issuable-header-text.hide-collapsed.pull-left
+          %strong
+            {{ issue.title }}
+          %br/
+          %span
+            = precede "#" do
+              {{ issue.id }}
+        %a.gutter-toggle.pull-right{ role: "button",
+          href: "#",
+          "@click.prevent" => "closeSidebar",
+          "aria-label" => "Toggle sidebar" }
+          = custom_icon("icon_close", size: 15)
+      .js-issuable-update
+        = render "projects/boards/components/sidebar/assignee"
+        = render "projects/boards/components/sidebar/milestone"
+        = render "projects/boards/components/sidebar/due_date"
+        = render "projects/boards/components/sidebar/labels"
+        = render "projects/boards/components/sidebar/notifications"
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..604e13858d13d9659fb27e85e99ff0a1fd7e344e
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -0,0 +1,40 @@
+.block.assignee
+  .title.hide-collapsed
+    Assignee
+    = icon("spinner spin", class: "block-loading")
+    - if can?(current_user, :admin_issue, @project)
+      = link_to "Edit", "#", class: "edit-link pull-right"
+  .value.hide-collapsed
+    %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
+      No assignee
+      - if can?(current_user, :admin_issue, @project)
+        \-
+        %a.js-assign-yourself{ href: "#" }
+          assign yourself
+    %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
+      "v-if" => "issue.assignee" }
+      %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
+        width: "32" }
+      %span.author
+        {{ issue.assignee.name }}
+      %span.username
+        = precede "@" do
+          {{ issue.assignee.username }}
+  - if can?(current_user, :admin_issue, @project)
+    .selectbox.hide-collapsed
+      %input{ type: "hidden",
+        name: "issue[assignee_id]",
+        id: "issue_assignee_id",
+        ":value" => "issue.assignee.id",
+        "v-if" => "issue.assignee" }
+      .dropdown
+        %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
+          ":data-issuable-id" => "issue.id",
+          ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+          Select assignee
+          = icon("chevron-down")
+        .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+          = dropdown_title("Assign to")
+          = dropdown_filter("Search users")
+          = dropdown_content
+          = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c7da1d0d4acf4b643ce72ae89198ee3dcb7b4555
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -0,0 +1,32 @@
+.block.due_date
+  .title
+    Due date
+    = icon("spinner spin", class: "block-loading")
+    - if can?(current_user, :admin_issue, @project)
+      = link_to "Edit", "#", class: "edit-link pull-right"
+  .value
+    .value-content
+      %span.no-value{ "v-if" => "!issue.dueDate" }
+        No due date
+      %span.bold{ "v-if" => "issue.dueDate" }
+        {{ issue.dueDate | due-date }}
+      - if can?(current_user, :admin_issue, @project)
+        %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
+          \-
+          %a.js-remove-due-date{ href: "#", role: "button" }
+            remove due date
+  - if can?(current_user, :admin_issue, @project)
+    .selectbox
+      %input{ type: "hidden",
+        name: "issue[due_date]",
+        ":value" => "issue.dueDate" }
+      .dropdown
+        %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
+          data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
+          ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+          %span.dropdown-toggle-text Due date
+          = icon('chevron-down')
+        .dropdown-menu.dropdown-menu-due-date
+          = dropdown_title('Due date')
+          = dropdown_content do
+            .js-due-date-calendar
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ce68e5e1998d0670d5623a410b0112112d38ffb9
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -0,0 +1,30 @@
+.block.labels
+  .title
+    Labels
+    = icon("spinner spin", class: "block-loading")
+    - if can?(current_user, :admin_issue, @project)
+      = link_to "Edit", "#", class: "edit-link pull-right"
+  .value.issuable-show-labels
+    %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
+      None
+    %a{ href: "#",
+      "v-for" => "label in issue.labels" }
+      %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+        {{ label.title }}
+  - if can?(current_user, :admin_issue, @project)
+    .selectbox
+      %input{ type: "hidden",
+        name: "issue[label_names][]",
+        "v-for" => "label in issue.labels",
+        ":value" => "label.id" }
+      .dropdown
+        %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
+          data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) },
+          ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+          %span.dropdown-toggle-text
+            Label
+          = icon('chevron-down')
+        .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
+            = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3cd20d1c0f769da70ad8d45e9bfa437f7940356e
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -0,0 +1,28 @@
+.block.milestone
+  .title
+    Milestone
+    = icon("spinner spin", class: "block-loading")
+    - if can?(current_user, :admin_issue, @project)
+      = link_to "Edit", "#", class: "edit-link pull-right"
+  .value
+    %span.no-value{ "v-if" => "!issue.milestone" }
+      None
+    %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
+      {{ issue.milestone.title }}
+  - if can?(current_user, :admin_issue, @project)
+    .selectbox
+      %input{ type: "hidden",
+        ":value" => "issue.milestone.id",
+        name: "issue[milestone_id]",
+        "v-if" => "issue.milestone" }
+      .dropdown
+        %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+          ":data-issuable-id" => "issue.id",
+          ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+          Milestone
+          = icon("chevron-down")
+        .dropdown-menu.dropdown-select.dropdown-menu-selectable
+          = dropdown_title("Assignee milestone")
+          = dropdown_filter("Search milestones")
+          = dropdown_content
+          = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..21c9563e9db9f9f5416df01820cb1df9c998f676
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -0,0 +1,11 @@
+- if current_user
+  .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
+    .title
+      Notifications
+    %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+      {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
+    .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
+      .unsubscribed{ "v-show" => "!issue.subscribed" }
+        You're not receiving notifications from this thread.
+      .subscribed{ "v-show" => "issue.subscribed" }
+        You're receiving notifications because you're subscribed to this thread.
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..29c9a43a0c1dc85d457559ab6345f23a8631f1be
--- /dev/null
+++ b/app/views/projects/boards/index.html.haml
@@ -0,0 +1,18 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('boards/boards_bundle.js')
+  = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+  .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+    .boards-app-loading.text-center{ "v-if" => "loading" }
+      = icon("spinner spin")
+    = render "projects/boards/components/board"
+  = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..29c9a43a0c1dc85d457559ab6345f23a8631f1be
--- /dev/null
+++ b/app/views/projects/boards/show.html.haml
@@ -0,0 +1,18 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('boards/boards_bundle.js')
+  = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+  .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+    .boards-app-loading.text-center{ "v-if" => "loading" }
+      = icon("spinner spin")
+    = render "projects/boards/components/board"
+  = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 4bd85061240a86b2859c0f7b65880c79fe79a7f9..9135cee8364e9c2f40e79f200ee6b426f8a7c6f9 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,12 +1,13 @@
-- commit = @repository.commit(branch.target)
+- commit = @repository.commit(branch.dereferenced_target)
 - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
 - diverging_commit_counts = @repository.diverging_commit_counts(branch)
 - number_commits_behind = diverging_commit_counts[:behind]
 - number_commits_ahead = diverging_commit_counts[:ahead]
+- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
 %li(class="js-branch-#{branch.name}")
   %div
-    = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
-      %span.item-title.str-truncated= branch.name
+    = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
+      = branch.name
     &nbsp;
     - if branch.name == @repository.root_ref
       %span.label.label-primary default
@@ -19,16 +20,23 @@
         %i.fa.fa-lock
         protected
     .controls.hidden-xs
-      - if create_mr_button?(@repository.root_ref, branch.name)
+      - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
         = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
           Merge Request
 
       - if branch.name != @repository.root_ref
-        = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
+        = 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
 
-      - if can_remove_branch?(@project, branch.name)
-        = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+      = render 'projects/buttons/download', project: @project, ref: 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'}",
+          method: :delete,
+          data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
+          remote: true,
+          "aria-label" => "Delete branch" do
           = icon("trash-o")
 
     - if branch.name != @repository.root_ref
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index d54c76ff9c813d22f01a68ceba0cc3b5b9588efa..de607772df61e92101f56b6259988c6499a64eb9 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,4 +1,6 @@
 .branch-commit
+  .icon-container.commit-icon
+    = custom_icon("icon_commit")
   = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace"
   &middot;
   %span.str-truncated
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index e889f29c81605e021a97f5ab8bfec0359c1e9e4c..84f38575e847d36c2c979eab3e9221e1095ceb0b 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,7 +15,7 @@
         %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
           %span.light
             = projects_sort_options_hash[@sort]
-          %b.caret
+          = icon('caret-down')
         %ul.dropdown-menu.dropdown-menu-align-right
           %li
             = link_to filter_branches_path(sort: sort_value_name) do
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 51b5bd9db427d53a5c0c78d76de0edfbcb4370e0..3f2ce7377fdee1b3786159dc71e438f7e8faf0df 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,16 +1,19 @@
 .content-block.build-header
-  = ci_status_with_icon(@build.status)
-  Build
-  %strong ##{@build.id}
-  for commit
-  = link_to ci_status_path(@build.pipeline) do
-    %strong= @build.pipeline.short_sha
-  from
-  = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
-    %code
-      = @build.ref
-  - if @build.user
-    = render "user"
-  = time_ago_with_tooltip(@build.created_at)
+  .header-content
+    = ci_status_with_icon(@build.status)
+    Build
+    %strong ##{@build.id}
+    for commit
+    = link_to ci_status_path(@build.pipeline) do
+      %strong= @build.pipeline.short_sha
+    from
+    = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
+      %code
+        = @build.ref
+    - if @build.user
+      = render "user"
+    = time_ago_with_tooltip(@build.created_at)
+  - if can?(current_user, :update_build, @build) && @build.retryable?
+    = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', 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/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index a8bc53c284995b93a4466d95707e49e122ef84b2..28f519f11b256d93220b3ea24314322ee85429bd 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,3 +1,5 @@
+- builds = @build.pipeline.builds.to_a
+
 %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
   .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
     Build
@@ -5,104 +7,132 @@
     %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
       = icon('angle-double-right')
   - if @build.coverage
-    .block.block-first
+    .block.coverage
       .title
         Test coverage
       %p.build-detail-row
         #{@build.coverage}%
 
-  - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
-    .block{ class: ("block-first" if !@build.coverage) }
-      .title
-        Build artifacts
-      - if @build.artifacts_expired?
-        %p.build-detail-row
-          The artifacts were removed
-          #{time_ago_with_tooltip(@build.artifacts_expire_at)}
-      - elsif @build.artifacts_expire_at
-        %p.build-detail-row
-          The artifacts will be removed in
-          %span.js-artifacts-remove= @build.artifacts_expire_at
+  .blocks-container
+    - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+      .block{ class: ("block-first" if !@build.coverage) }
+        .title
+          Build artifacts
+        - if @build.artifacts_expired?
+          %p.build-detail-row
+            The artifacts were removed
+            #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+        - elsif @build.artifacts_expire_at
+          %p.build-detail-row
+            The artifacts will be removed in
+            %span.js-artifacts-remove= @build.artifacts_expire_at
 
-      - if @build.artifacts?
-        .btn-group.btn-group-justified{ role: :group }
-          - if @build.artifacts_expire_at
-            = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
-              Keep
+        - if @build.artifacts?
+          .btn-group.btn-group-justified{ role: :group }
+            - if @build.artifacts_expire_at
+              = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+                Keep
 
-          = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
-            Download
+            = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+              Download
 
-          - if @build.artifacts_metadata?
-            = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
-              Browse
+            - if @build.artifacts_metadata?
+              = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+                Browse
 
-  .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
-    .title
-      Build details
-      - if can?(current_user, :update_build, @build) && @build.retryable?
-        = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
-    - if @build.merge_request
-      %p.build-detail-row
-        %span.build-light-text Merge Request:
-        = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
-    - if @build.duration
-      %p.build-detail-row
-        %span.build-light-text Duration:
-        = time_interval_in_words(@build.duration)
-    - if @build.finished_at
-      %p.build-detail-row
-        %span.build-light-text Finished:
-        #{time_ago_with_tooltip(@build.finished_at)}
-    - if @build.erased_at
+    .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
+      .title
+        Build details
+        - if can?(current_user, :update_build, @build) && @build.retryable?
+          = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+      - if @build.merge_request
+        %p.build-detail-row
+          %span.build-light-text Merge Request:
+          = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
+      - if @build.duration
+        %p.build-detail-row
+          %span.build-light-text Duration:
+          = time_interval_in_words(@build.duration)
+      - if @build.finished_at
+        %p.build-detail-row
+          %span.build-light-text Finished:
+          #{time_ago_with_tooltip(@build.finished_at)}
+      - if @build.erased_at
+        %p.build-detail-row
+          %span.build-light-text Erased:
+          #{time_ago_with_tooltip(@build.erased_at)}
       %p.build-detail-row
-        %span.build-light-text Erased:
-        #{time_ago_with_tooltip(@build.erased_at)}
-    %p.build-detail-row
-      %span.build-light-text Runner:
-      - if @build.runner && current_user && current_user.admin
-        = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
-      - elsif @build.runner
-        \##{@build.runner.id}
-    .btn-group.btn-group-justified{ role: :group }
-      - if @build.has_trace?
-        = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
-      - if @build.active?
-        = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
-      - if can?(current_user, :update_build, @project) && @build.erasable?
-        = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
-                  class: "btn btn-sm btn-default", method: :post,
-                  data: { confirm: "Are you sure you want to erase this build?" } do
-          Erase
+        %span.build-light-text Runner:
+        - if @build.runner && current_user && current_user.admin
+          = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+        - elsif @build.runner
+          \##{@build.runner.id}
+      .btn-group.btn-group-justified{ role: :group }
+        - if @build.has_trace_file?
+          = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
+        - if @build.active?
+          = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+        - if can?(current_user, :update_build, @project) && @build.erasable?
+          = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+                    class: "btn btn-sm btn-default", method: :post,
+                    data: { confirm: "Are you sure you want to erase this build?" } do
+            Erase
 
-  - if @build.trigger_request
-    .build-widget
-      %h4.title
-        Trigger
+    - if @build.trigger_request
+      .build-widget
+        %h4.title
+          Trigger
 
-      %p
-        %span.build-light-text Token:
-        #{@build.trigger_request.trigger.short_token}
-
-      - if @build.trigger_request.variables
         %p
-          %span.build-light-text Variables:
+          %span.build-light-text Token:
+          #{@build.trigger_request.trigger.short_token}
 
+        - if @build.trigger_request.variables
+          %p
+            %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
 
-        - @build.trigger_request.variables.each do |key, value|
-          %code
-            #{key}=#{value}
 
-  .block
-    .title
-      Commit title
-    %p.build-light-text.append-bottom-0
-      #{@build.pipeline.git_commit_title}
+          - @build.trigger_request.variables.each do |key, value|
+            .hide.js-build
+              .js-build-variable= key
+              .js-build-value= value
 
-  - if @build.tags.any?
     .block
       .title
-        Tags
-      - @build.tag_list.each do |tag|
-        %span.label.label-primary
-          = tag
+        Commit title
+      %p.build-light-text.append-bottom-0
+        #{@build.pipeline.git_commit_title}
+
+    - if @build.tags.any?
+      .block
+        .title
+          Tags
+        - @build.tag_list.each do |tag|
+          %span.label.label-primary
+            = tag
+
+    - if @build.pipeline.stages.many?
+      .dropdown.build-dropdown
+        .title Stage
+        %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+          %span.stage-selection More
+          = icon('caret-down')
+        %ul.dropdown-menu
+          - @build.pipeline.stages.each do |stage|
+            %li
+              %a.stage-item= stage
+
+  .builds-container
+    - HasStatus::ORDERED_STATUSES.each do |build_status|
+      - builds.select{|build| build.status == build_status}.each do |build|
+        .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
+          = link_to namespace_project_build_path(@project.namespace, @project, build) do
+            = icon('arrow-right')
+            = ci_icon_for_status(build.status)
+            %span
+              - if build.name
+                = build.name
+              - else
+                = build.id
+            - if build.retried?
+              %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..36294c89fa807976f75cbe9f20b1b6e5749c9af5
--- /dev/null
+++ b/app/views/projects/builds/_table.html.haml
@@ -0,0 +1,24 @@
+- admin = local_assigns.fetch(:admin, false)
+
+- if builds.blank?
+  %div
+    .nothing-here-block No builds to show
+- else
+  .table-holder
+    %table.table.ci-table.builds-page
+      %thead
+        %tr
+          %th Status
+          %th Build
+          - if admin
+            %th Project
+            %th Runner
+          %th Stage
+          %th Name
+          %th
+          %th Coverage
+          %th
+
+      = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
+
+  = paginate builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml
index 2642de8021df5c5f59ccea327d5923889d4a1984..83f299da651f82fb6460974526e572a6b3397c7a 100644
--- a/app/views/projects/builds/_user.html.haml
+++ b/app/views/projects/builds/_user.html.haml
@@ -1,4 +1,7 @@
 by
 %a{ href: user_path(@build.user) }
-  = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
-  %strong= @build.user.to_reference
+  %span.hidden-xs
+    = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
+    %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
+      = @build.user.name
+  %strong.visible-xs-inline= @build.user.to_reference
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 2af625f69cd378e0b66786baf7f1d9d5012459c0..06070f12bbd13dd76dae17d3cc96eea8ff7937f5 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -4,30 +4,8 @@
 
 %div{ class: container_class }
   .top-area
-    %ul.nav-links
-      %li{class: ('active' if @scope.nil?)}
-        = link_to project_builds_path(@project) do
-          All
-          %span.badge.js-totalbuilds-count
-            = number_with_delimiter(@all_builds.count(:id))
-
-      %li{class: ('active' if @scope == 'pending')}
-        = link_to project_builds_path(@project, scope: :pending) do
-          Pending
-          %span.badge
-            = number_with_delimiter(@all_builds.pending.count(:id))
-
-      %li{class: ('active' if @scope == 'running')}
-        = link_to project_builds_path(@project, scope: :running) do
-          Running
-          %span.badge
-            = number_with_delimiter(@all_builds.running.count(:id))
-
-      %li{class: ('active' if @scope == 'finished')}
-        = link_to project_builds_path(@project, scope: :finished) do
-          Finished
-          %span.badge
-            = number_with_delimiter(@all_builds.finished.count(:id))
+    - build_path_proc = ->(scope) { project_builds_path(@project, scope: scope) }
+    = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
 
     .nav-controls
       - if can?(current_user, :update_build, @project)
@@ -41,24 +19,5 @@
         = link_to ci_lint_path, class: 'btn btn-default' do
           %span CI Lint
 
-  %ul.content-list.builds-content-list
-    - if @builds.blank?
-      %li
-        .nothing-here-block No builds to show
-    - else
-      .table-holder
-        %table.table.builds
-          %thead
-            %tr
-              %th Status
-              %th Commit
-              %th Stage
-              %th Name
-              %th
-              - if @project.build_coverage_enabled?
-                %th Coverage
-              %th
-
-          = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
-
-      = paginate @builds, theme: 'gitlab'
+  %div.content-list.builds-content-list
+    = render "table", builds: @builds, project: @project
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 4421f3b9562e3cb40e0cc87252f1d8c5e3d390a0..ae7a7ecb392e8eba965776a519efc8f62e68dc72 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,70 +1,53 @@
+- @no_container = true
 - page_title "#{@build.name} (##{@build.id})", "Builds"
-- trace_with_state = @build.trace_with_state
 - header_title project_title(@project, "Builds", project_builds_path(@project))
-
-.build-page
-  = render "header"
-
-  - builds = @build.pipeline.builds.latest.to_a
-  - if builds.size > 1
-    %ul.nav-links.no-top.no-bottom
-      - builds.each do |build|
-        %li{class: ('active' if build == @build) }
-          = link_to namespace_project_build_path(@project.namespace, @project, build) do
-            = ci_icon_for_status(build.status)
-            %span
-              - if build.name
-                = build.name
-              - else
-                = build.id
-
-      - if @build.retried?
-        %li.active
-          %a
-            Build ##{@build.id}
-            &middot;
-            %i.fa.fa-warning
-            This build was retried.
-  - if @build.stuck?
-    - unless @build.any_runners_online?
-      .bs-callout.bs-callout-warning
-        %p
-          - if no_runners_for_project?(@build.project)
-            This build is stuck, because the project doesn't have any runners online assigned to it.
-          - elsif @build.tags.any?
-            This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
-            - @build.tags.each do |tag|
-              %span.label.label-primary
-                = tag
-          - else
-            This build is stuck, because you don't have any active runners that can run this build.
-
-          %br
-          Go to
-          = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
-            Runners page
-
-  .prepend-top-default
-    - if @build.active?
-      .autoscroll-container
-        %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
-    - if @build.erased?
-      .erased.alert.alert-warning
-        - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
-        Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
-    - else
-      #js-build-scroll.scroll-controls
-        = link_to '#build-trace', class: 'btn' do
-          %i.fa.fa-angle-up
-        = link_to '#down-build-trace', class: 'btn' do
-          %i.fa.fa-angle-down
-      %pre.build-trace#build-trace
-        %code.bash.js-build-output
-        = icon("refresh spin", class: "js-build-refresh")
-
-    #down-build-trace
-
-= render "sidebar"
-
-:javascript
-  new Build("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
+= render "projects/pipelines/head", build_subnav: true
+
+%div{ class: container_class }
+  .build-page
+    = render "header"
+
+    - if @build.stuck?
+      - unless @build.any_runners_online?
+        .bs-callout.bs-callout-warning
+          %p
+            - if no_runners_for_project?(@build.project)
+              This build is stuck, because the project doesn't have any runners online assigned to it.
+            - elsif @build.tags.any?
+              This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+              - @build.tags.each do |tag|
+                %span.label.label-primary
+                  = tag
+            - else
+              This build is stuck, because you don't have any active runners that can run this build.
+
+            %br
+            Go to
+            = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
+              Runners page
+
+    .prepend-top-default
+      - if @build.erased?
+        .erased.alert.alert-warning
+          - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
+          Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+      - else
+        #js-build-scroll.scroll-controls
+          .scroll-step
+            = link_to '#build-trace', class: 'btn' do
+              %i.fa.fa-angle-up
+            = link_to '#down-build-trace', class: 'btn' do
+              %i.fa.fa-angle-down
+          - if @build.active?
+            .autoscroll-container
+              %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
+                Enable autoscroll
+        %pre.build-trace#build-trace
+          %code.bash.js-build-output
+          = icon("refresh spin", class: "js-build-refresh")
+
+        #down-build-trace
+
+  = render "sidebar"
+
+.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 58f43ecb5d5f2341d08bdc45110e394b67438511..7e83a88913ad3d42ec16a32916d95f59673bc07d 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,42 @@
-- unless @project.empty_repo?
-  - if can? current_user, :download_code, @project
-    = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
-      = icon('download')
+- if !project.empty_repo? && can?(current_user, :download_code, project)
+  %span{class: 'hidden-xs hidden-sm download-button'}
+    .dropdown.inline
+      %button.btn{ 'data-toggle' => 'dropdown' }
+        = icon('download')
+        = icon("caret-down")
+        %span.sr-only
+          Select Archive Format
+      %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+        %li.dropdown-header Source code
+        %li
+          = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+            %i.fa.fa-download
+            %span Download zip
+        %li
+          = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+            %i.fa.fa-download
+            %span Download tar.gz
+        %li
+          = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+            %i.fa.fa-download
+            %span Download tar.bz2
+        %li
+          = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+            %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?
+            %li.dropdown-header Artifacts
+            - unless pipeline.latest?
+              - latest_pipeline = project.pipeline_for(ref)
+              %li
+                .unclickable= ci_status_for_statuseable(latest_pipeline)
+              %li.dropdown-header Previous Artifacts
+            - artifacts.each do |job|
+              %li
+                = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
+                  %i.fa.fa-download
+                  %span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index ca907077c2b33fce8d190addbc173b1561ed00b9..6cd9b98a706d236560e5b4f10c4f2d7a3d8d6b9a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,7 +1,8 @@
 - if current_user
-  .btn-group
+  .dropdown.inline.project-dropdown
     %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
       = icon('plus')
+      = icon("caret-down")
     %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
       - can_create_issue = can?(current_user, :create_issue, @project)
       - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index d78888e9fe4aef6e60b3934a13a94f4ee908295b..27da86b9efe614dc81e70b0b9a82c932a05ee092 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -3,12 +3,12 @@
     - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
       = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
         = custom_icon('icon_fork')
-        Fork
+        %span Fork
     - else
-      = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
+      = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
         = custom_icon('icon_fork')
-        Fork
+        %span Fork
     %div.count-with-arrow
       %span.arrow
-      = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
+      = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
         = @project.forks_count
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..fdc80d44253bd049a71cddce5c432b082d7c5778
--- /dev/null
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -0,0 +1,7 @@
+- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
+  - if @repository.koding_yml
+    = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
+      Run in IDE (Koding)
+  - else
+    = link_to add_koding_stack_path(@project), class: 'btn' do
+      Set Up Koding
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 71cf5582a4cd66668ca776511ea648054245b6b8..12d351017705883037bd44298d53a7e3db6b722a 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,10 @@
 - if current_user
-  = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
+  = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
     - if current_user.starred?(@project)
-      = icon('star fw')
+      = icon('star')
       %span.starred Unstar
     - else
-      = icon('star-o fw')
+      = icon('star-o')
       %span Star
   %div.count-with-arrow
     %span.arrow
@@ -13,7 +13,7 @@
 
 - else
   = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
-    = icon('star fw')
+    = icon('star')
     Star
   %div.count-with-arrow
     %span.arrow
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 91081435220cab25fb66d81b13ca2477c6df1345..94632056b154fb1524df200a9ac894c7ae6ddcf6 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -1,58 +1,69 @@
-%tr.build.commit
+- admin = local_assigns.fetch(:admin, false)
+- ref = local_assigns.fetch(:ref, nil)
+- commit_sha = local_assigns.fetch(:commit_sha, nil)
+- retried = local_assigns.fetch(:retried, false)
+- stage = local_assigns.fetch(:stage, false)
+- coverage = local_assigns.fetch(:coverage, false)
+- allow_retry = local_assigns.fetch(:allow_retry, false)
+
+%tr.build.commit{class: ('retried' if retried)}
   %td.status
     - if can?(current_user, :read_build, build)
       = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
     - else
       = ci_status_with_icon(build.status)
 
-  %td
-    .branch-commit
-      - if can?(current_user, :read_build, build)
-        = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
-          %span ##{build.id}
-      - else
-        %span ##{build.id}
+  %td.branch-commit
+    - if can?(current_user, :read_build, build)
+      = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+        %span.build-link ##{build.id}
+    - else
+      %span.build-link ##{build.id}
 
-      - if defined?(ref) && ref
-        - if build.ref
-          .icon-container
-            = build.tag? ? icon('tag') : icon('code-fork')
-          = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
-        - else
-          .light none
+    - if ref
+      - if build.ref
         .icon-container
-          = custom_icon("icon_commit")
+          = build.tag? ? icon('tag') : icon('code-fork')
+        = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
+      - else
+        .light none
+      .icon-container.commit-icon
+        = custom_icon("icon_commit")
+
+    - if commit_sha
+      = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
 
-      - if defined?(commit_sha) && commit_sha
-        = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
+    - if build.stuck?
+      = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
 
-      - if build.stuck?
-        = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
-      - if defined?(retried) && retried
-        = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+    - if retried
+      = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
 
-      .label-container
-        - if build.tags.any?
-          - build.tags.each do |tag|
-            %span.label.label-primary
-              = tag
-        - if build.try(:trigger_request)
-          %span.label.label-info triggered
-        - if build.try(:allow_failure)
-          %span.label.label-danger allowed to fail
-        - if defined?(retried) && retried
-          %span.label.label-warning retried
-        - if build.manual?
-          %span.label.label-info manual
+    .label-container
+      - if build.tags.any?
+        - build.tags.each do |tag|
+          %span.label.label-primary
+            = tag
+      - if build.try(:trigger_request)
+        %span.label.label-info triggered
+      - if build.try(:allow_failure)
+        %span.label.label-danger allowed to fail
+      - if build.manual?
+        %span.label.label-info manual
 
-  - if defined?(runner) && runner
+  - if admin
+    %td
+      - if build.project
+        = link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project)
+
+  - if admin
     %td
       - if build.try(:runner)
         = runner_link(build.runner)
       - else
         .light none
 
-  - if defined?(stage) && stage
+  - if stage
     %td
       = build.stage
 
@@ -63,14 +74,15 @@
     - if build.duration
       %p.duration
         = custom_icon("icon_timer")
-        = duration_in_numbers(build.finished_at, build.started_at)
+        = duration_in_numbers(build.duration)
+
     - if build.finished_at
       %p.finished-at
         = icon("calendar")
         %span #{time_ago_with_tooltip(build.finished_at)}
 
-  - if defined?(coverage) && coverage
-    %td.coverage
+  %td.coverage
+    - if coverage
       - if build.try(:coverage)
         #{build.coverage}%
 
@@ -83,10 +95,10 @@
         - if build.active?
           = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
             = icon('remove', class: 'cred')
-        - elsif defined?(allow_retry) && allow_retry
+        - elsif allow_retry
           - if build.retryable?
             = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
               = icon('repeat')
-          - elsif build.playable?
+          - elsif build.playable? && !admin
             = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
-              = icon('play')
+              = custom_icon('icon_play')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..93dca81e6f9ae39e95720c4f796259c33b4aed51
--- /dev/null
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -0,0 +1,13 @@
+- is_playable = subject.playable? && can?(current_user, :update_build, @project)
+- if is_playable
+  = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
+    = ci_icon_for_status('play')
+    .ci-status-text= subject.name
+- elsif can?(current_user, :read_build, @project)
+  = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
+    %span.ci-status-icon
+      = ci_icon_for_status(subject.status)
+    .ci-status-text= subject.name
+- else
+  %span.ci-status-icon
+    = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 78709a92aedf405fbf7cd787b05a05aa0634fe9d..2a2d24be736e8d361cfc7791ae3d1b43c4719608 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,62 +1,68 @@
 - status = pipeline.status
+- show_commit = local_assigns.fetch(:show_commit, true)
+- show_branch = local_assigns.fetch(:show_branch, true)
+
 %tr.commit
   %td.commit-link
-    = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
+    = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
       = ci_status_with_icon(status)
 
-
   %td
-    .branch-commit
-      = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
-        %span ##{pipeline.id}
-      - if pipeline.ref
-        .icon-container
-          = pipeline.tag? ? icon('tag') : icon('code-fork')
-        = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
-        .icon-container
-          = custom_icon("icon_commit")
-      = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
-      - if pipeline.latest?
-        %span.label.label-success.has-tooltip{ title: 'Latest build 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
+    = 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 build 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
 
-      %p.commit-title
-        - if commit = pipeline.commit
-          = author_avatar(commit, size: 20)
-          = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
-        - else
-          Cant find HEAD commit for this branch
+  %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
 
-    - stages_status = pipeline.statuses.relevant.latest.stages_status
+  - stages_status = pipeline.statuses.latest.stages_status
+  %td.stage-cell
     - stages.each do |stage|
-      %td.stage-cell
-        - status = stages_status[stage]
-        - tooltip = "#{stage.titleize}: #{status || 'not found'}"
-        - if status
-          = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+      - status = stages_status[stage]
+      - tooltip = "#{stage.titleize}: #{status || 'not found'}"
+      - if status
+        .stage-container
+          = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
             = ci_icon_for_status(status)
-        - else
-          .light.has-tooltip{ title: tooltip }
-            \-
 
   %td
-    - if pipeline.started_at && pipeline.finished_at
+    - if pipeline.duration
       %p.duration
         = custom_icon("icon_timer")
-        = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+        = duration_in_numbers(pipeline.duration)
     - if pipeline.finished_at
       %p.finished-at
         = icon("calendar")
-        #{time_ago_with_tooltip(pipeline.finished_at, short_format: true, skip_js: true)}
+        #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
 
-  %td.pipeline-actions
-    .controls.hidden-xs.pull-right
+  %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?
@@ -64,31 +70,31 @@
           - if actions.any?
             .btn-group
               %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
-                = icon("play")
-                %b.caret
+                = custom_icon('icon_play')
+                = icon('caret-down')
               %ul.dropdown-menu.dropdown-menu-align-right
                 - actions.each do |build|
                   %li
-                    = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do
-                      = icon("play")
+                    = 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.humanize
           - if artifacts.present?
             .btn-group
               %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
                 = icon("download")
-                %b.caret
+                = icon('caret-down')
               %ul.dropdown-menu.dropdown-menu-align-right
                 - artifacts.each do |build|
                   %li
-                    = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
+                    = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do
                       = icon("download")
                       %span Download '#{build.name}' artifacts
 
-      - if can?(current_user, :update_pipeline, @project)
+      - if can?(current_user, :update_pipeline, pipeline.project)
         .cancel-retry-btns.inline
           - if pipeline.retryable?
-            = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+            = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
               = icon("repeat")
           - if pipeline.cancelable?
-            = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+            = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
               = icon("remove")
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index a508382578a7c9f24492317fb8412ff9771972f6..b7087749428bec7659c5e5e6ecf09cef13ad5e94 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,2 +1,2 @@
-- @pipelines.each do |pipeline|
+- @ci_pipelines.each do |pipeline|
   = render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index d9b800a4ded8da79719e96df1e13c80b804722bf..e4cd55b9f7a9c976321ea910106146ca41f4162b 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,9 @@
           .form-group.branch
             = label_tag 'target_branch', target_label, class: 'control-label'
             .col-sm-10
-              = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
+              = 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 }})
+
               - if can?(current_user, :push_code, @project)
                 .js-create-merge-request-container
                   .checkbox
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 935433306ea020d1bc58a645e7fc289596ff6fd6..cbfd99ca4482ca72b3d1f737512950d3b99e080b 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -3,6 +3,11 @@
     = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
       Changes
       %span.badge= @diffs.size
+  - if can?(current_user, :read_pipeline, @project)
+    = nav_link(path: 'commit#pipelines') do
+      = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+        Pipelines
+        %span.badge= @ci_pipelines.count
   = nav_link(path: 'commit#builds') do
     = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
       Builds
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
index 9d925cacc0d2adfef5c06620a6709019a3a7c47f..6bb900e3fc1f34f2b77cfb4ccfc6d47308bb4825 100644
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -8,8 +8,8 @@
       - if stage
         &nbsp;
         = stage.titleize
-  = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
-  = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
+  = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
+  = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
   %tr
     %td{colspan: 10}
       &nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 3ad866bb2f1a6fa1a913580df00fb0d3e804300a..0ebc38d16cf95c4bc7ba6f708125502682a0d336 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,10 +1,25 @@
 .commit-info-row.commit-info-row-header
-  %span.hidden-xs Authored by
-  %strong
-    = commit_author_link(@commit, avatar: true, size: 24)
-  #{time_ago_with_tooltip(@commit.authored_date)}
-
-  .pull-right.commit-action-buttons
+  .commit-meta
+    %strong Commit
+    %strong.monospace.js-details-short= @commit.short_id
+    = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+      %span.text-expander
+        \...
+    %span.js-details-content.hide
+      %strong.monospace.commit-hash-full= @commit.id
+    = clipboard_button(clipboard_text: @commit.id)
+    %span.hidden-xs authored
+    #{time_ago_with_tooltip(@commit.authored_date)}
+    %span by
+    = author_avatar(@commit, size: 24)
+    %strong
+      = commit_author_link(@commit, avatar: true, size: 24)
+    - if @commit.different_committer?
+      %span.light Committed by
+      %strong
+        = commit_committer_link(@commit, avatar: true, size: 24)
+      #{time_ago_with_tooltip(@commit.committed_date)}
+  .commit-action-buttons
     - if defined?(@notes_count) && @notes_count > 0
       %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
         = icon('comment')
@@ -13,8 +28,8 @@
       Browse Files
     .dropdown.inline
       %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
-        %span.hidden-xs Options
-        %span.caret.commit-options-dropdown-caret
+        %span Options
+        = icon('caret-down')
       %ul.dropdown-menu.dropdown-menu-align-right
         %li.visible-xs-block.visible-sm-block
           = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
@@ -24,6 +39,8 @@
             = 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)
         %li.divider
         %li.dropdown-header
           Download
@@ -31,42 +48,35 @@
           %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
         %li= link_to "Plain Diff",    namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff)
 
-- if @commit.different_committer?
-  .commit-info-row
-    %span.light Committed by
-    %strong
-      = commit_committer_link(@commit, avatar: true, size: 24)
-    #{time_ago_with_tooltip(@commit.committed_date)}
-
-.commit-info-row
-  %span.hidden-xs.hidden-sm Commit
-  = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm"
-  = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline"
-  = clipboard_button(clipboard_text: @commit.id)
-  %span.cgray= pluralize(@commit.parents.count, "parent")
-  - @commit.parents.each do |parent|
-    = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
-
-  %span.commit-info.branches
-    %i.fa.fa-spinner.fa-spin
-
-- if @commit.status
-  .commit-info-row
-    Builds for
-    = pluralize(@commit.pipelines.count, 'pipeline')
-    = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
-      = ci_icon_for_status(@commit.status)
-      = ci_label_for_status(@commit.status)
-    - if @commit.pipelines.duration
-      in
-      = time_interval_in_words @commit.pipelines.duration
-
-.commit-box.content-block
+.commit-box
   %h3.commit-title
-    = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
+    = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
   - if @commit.description.present?
     %pre.commit-description
-      = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
+      = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
+
+.commit-info-widget
+  .widget-row.branch-info
+    .icon-container.commit-icon
+      = custom_icon("icon_commit")
+    %span.cgray= pluralize(@commit.parents.count, "parent")
+    - @commit.parents.each do |parent|
+      = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
+    %span.commit-info.branches
+      %i.fa.fa-spinner.fa-spin
+
+  - if @commit.status
+    .widget-row.pipeline-info
+      .icon-container
+        = ci_icon_for_status(@commit.status)
+      Pipeline
+      = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.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)
+      in
+      = time_interval_in_words @commit.pipelines.total_duration
 
 :javascript
   $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 640abdb993fa09c441c23f63c7e6a5a37cb1d6ad..d6916fb7f1ac2089d2aa886e4d244ea25c781f6c 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,27 +1,47 @@
-.row-content-block.build-content.middle-block
-  .pull-right
-    - if can?(current_user, :update_pipeline, pipeline.project)
-      - if pipeline.builds.latest.failed.any?(&:retryable?)
-        = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+.pipeline-graph-container
+  .row-content-block.build-content.middle-block.pipeline-actions
+    .pull-right
+      .btn.btn-grouped.btn-white.toggle-pipeline-btn
+        %span.toggle-btn-text Hide
+        %span pipeline graph
+        %span.caret
+      - if can?(current_user, :update_pipeline, pipeline.project)
+        - if pipeline.builds.latest.failed.any?(&:retryable?)
+          = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
 
-      - if pipeline.builds.running_or_pending.any?
-        = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+        - if pipeline.builds.running_or_pending.any?
+          = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+
+    .oneline.clearfix
+      - if defined?(pipeline_details) && pipeline_details
+        Pipeline
+        = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
+        with
+        = pluralize pipeline.statuses.count(:id), "build"
+        - if pipeline.ref
+          for
+          = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
+        - if defined?(link_to_commit) && link_to_commit
+          for commit
+          = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
+        - if pipeline.duration
+          in
+          = time_interval_in_words pipeline.duration
+
+  .row-content-block.build-content.middle-block.pipeline-graph.hidden
+    .pipeline-visualization
+      %ul.stage-column-list
+        - stages = pipeline.stages_with_latest_statuses
+        - stages.each do |stage, statuses|
+          %li.stage-column
+            .stage-name
+              %a{name: stage}
+              - if stage
+                = stage.titleize
+            .builds-container
+              %ul
+                = render "projects/commit/pipeline_stage", statuses: statuses
 
-  .oneline.clearfix
-    - if defined?(pipeline_details) && pipeline_details
-      Pipeline
-      = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
-      with
-      = pluralize pipeline.statuses.count(:id), "build"
-      - if pipeline.ref
-        for
-        = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
-      - if defined?(link_to_commit) && link_to_commit
-        for commit
-        = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
-      - if pipeline.duration
-        in
-        = time_interval_in_words pipeline.duration
 
 - if pipeline.yaml_errors.present?
   .bs-callout.bs-callout-danger
@@ -36,7 +56,7 @@
     \.gitlab-ci.yml not found in this commit
 
 .table-holder.pipeline-holder
-  %table.table.builds.pipeline
+  %table.table.ci-table.pipeline
     %thead
       %tr
         %th Status
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..289aa5178b17092e847ce2cb52725a444f8488f0
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -0,0 +1,14 @@
+- status_groups = statuses.group_by(&:group_name)
+- status_groups.each do |group_name, grouped_statuses|
+  - if grouped_statuses.one?
+    - status = grouped_statuses.first
+    - is_playable = status.playable? && can?(current_user, :update_build, @project)
+    %li.build{ class: ("playable" if is_playable) }
+      .curve
+      .build-content
+        = render "projects/#{status.to_partial_path}_pipeline", subject: status
+  - else
+    %li.build
+      .curve
+      .dropdown.inline.build-content
+        = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..18daa2ee69375d35a571411e3a33b67626180379
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -0,0 +1,13 @@
+- group_status = CommitStatus.where(id: subject).status
+%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+  %span.ci-status-icon
+    = ci_icon_for_status(group_status)
+  %span.ci-status-text
+    = name
+  %span.badge= subject.size
+.dropdown-menu.grouped-pipeline-dropdown
+  .arrow
+  %ul
+    - subject.each do |status|
+      %li
+        = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2dc91a9b762803c865adc56581053e5f21472958
--- /dev/null
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -0,0 +1,15 @@
+%ul.content-list.pipelines
+  - if pipelines.blank?
+    %li
+      .nothing-here-block No pipelines to show
+  - else
+    .table-holder
+      %table.table.ci-table
+        %tbody
+          %th Status
+          %th Pipeline
+          %th Commit
+          %th Stages
+          %th
+          %th
+        = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 2f051fb90e076e3deac6de236963f0e502b4dcf0..077b2d2725b2c2c4c098fe5ad4f1ca4bff4254a1 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,7 +1,9 @@
+- @no_container = true
 - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/head"
 
-.prepend-top-default
+%div{ class: container_class }
   = render "commit_box"
 
-= render "ci_menu"
-= render "builds"
+  = render "ci_menu"
+  = render "builds"
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..8233e26e4e73794991e4bdd58e24d8332f35f498
--- /dev/null
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -0,0 +1,6 @@
+- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
+
+= render "commit_box"
+
+= render "ci_menu"
+= render "pipelines_list", pipelines: @ci_pipelines
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index ed44d86a687d0a1e36456d8b507650e12711c12b..b8c64d1f13e574d02bd1d85f0ff8075603cfadfa 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,14 +1,16 @@
+- @no_container = true
 - page_title        "#{@commit.title} (#{@commit.short_id})", "Commits"
 - page_description  @commit.description
+= render "projects/commits/head"
 
-.prepend-top-default
+%div{ class: container_class }
   = render "commit_box"
-- if @commit.status
-  = render "ci_menu"
-- else
-  %div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs
-= render "projects/notes/notes_with_form"
-- if can_collaborate_with_project?
-  - %w(revert cherry-pick).each do |type|
-    = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
+  - if @commit.status
+    = render "ci_menu"
+  - else
+    %div.block-connector
+  = render "projects/diffs/diffs", diffs: @diffs
+  = render "projects/notes/notes_with_form"
+  - if can_collaborate_with_project?
+    - %w(revert cherry-pick).each do |type|
+      = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index fd888f41b1ed86ac1db7abb4480383ef535091c8..34855c54176a8eadcacc1519e98f642cf83b1932 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,3 +1,4 @@
+- ref = local_assigns.fetch(:ref)
 - if @note_counts
   - note_count = @note_counts.fetch(commit.id, 0)
 - else
@@ -5,9 +6,9 @@
   - note_count = notes.user.count
 
 - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(commit.status) if commit.status
+- cache_key.push(commit.status(ref)) if commit.status(ref)
 
-= cache(cache_key) do
+= cache(cache_key, expires_in: 1.day) do
   %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
     = author_avatar(commit, size: 36)
 
@@ -18,22 +19,22 @@
           %span.commit-row-message.visible-xs-inline
             &middot;
             = commit.short_id
-          - if commit.status
+          - if commit.status(ref)
             .visible-xs-inline
-              = render_commit_status(commit)
+              = render_commit_status(commit, ref: ref)
           - if commit.description?
             %a.text-expander.hidden-xs.js-toggle-button ...
 
         .commit-actions.hidden-xs
-          - if commit.status
-            = render_commit_status(commit)
+          - if commit.status(ref)
+            = render_commit_status(commit, ref: ref)
           = clipboard_button(clipboard_text: commit.id)
           = 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)
 
       - if commit.description?
         %pre.commit-row-description.js-toggle-content
-          = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
+          = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
 
       .commit-row-info
         = commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 46e4de40042ff411bb3051ddd692f5b06d6509a4..ce416caa4946a2172cfb02bc8d565eb071f064e4 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= render commits, project: @project
+    %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 dd12eae8f7e0a304e6b7dce5a998ff8bf4b864b5..48756c68941a24b787dc194375b61379a1e7eaaf 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,13 +1,11 @@
-- unless defined?(project)
-  - project = @project
-
+- ref = local_assigns.fetch(:ref)
 - commits, hidden = limited_commits(@commits)
 
 - 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.list-unstyled.commit-list
-      = render commits, project: project
+      = render commits, project: project, ref: ref
 
 - if hidden > 0
   %li.alert.alert-warning
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 61152649907ddb7630ecd4ea9aaff97e9cb1a165..80763ce67caafa1b6422b1c23e3219b59e8fc8f7 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,30 +1,28 @@
-.scrolling-tabs-container.sub-nav-scroll
-  .fade-left
-    = icon('angle-left')
-  .fade-right
-    = icon('angle-right')
-  .nav-links.sub-nav.scrolling-tabs
-    %ul{ class: (container_class) }
-      = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
-        = link_to project_files_path(@project) do
-          Files
+= 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(controller: %w(tree blob blame edit_tree new_tree find_file)) do
+          = link_to project_files_path(@project) do
+            Files
 
-      = nav_link(controller: [:commit, :commits]) do
-        = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
-          Commits
+        = nav_link(controller: [:commit, :commits]) do
+          = 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: %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(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
+        = nav_link(html_options: {class: branches_tab_class}) do
+          = link_to namespace_project_branches_path(@project.namespace, @project) do
+            Branches
 
-      = nav_link(controller: [:tags, :releases]) do
-        = link_to namespace_project_tags_path(@project.namespace, @project) do
-          Tags
+        = nav_link(controller: [:tags, :releases]) do
+          = link_to namespace_project_tags_path(@project.namespace, @project) do
+            Tags
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9a44ba94970cbba016f37bbbd064c9ec9a4ef1ec..9628cbd163409b4d4026f657077fbb6f99a9f545 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -5,7 +5,8 @@
   - 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")
 
-= render "head"
+= content_for :sub_nav do
+  = render "head"
 
 %div{ class: container_class }
   .row-content-block.second-block.content-component-block
@@ -34,7 +35,7 @@
 
   %div{id: dom_id(@project)}
     %ol#commits-list.list-unstyled.content_list
-      = render "commits", project: @project
+      = render 'commits', project: @project, ref: @ref
   = spinner
 
 :javascript
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d79336f5a6035e4cef05bd618d5765d4dc84c2fa..7bde20c32868b2401c9d39eb1a4cdb6050c5f47e 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,17 +1,22 @@
 = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
   .clearfix
     - if params[:to] && params[:from]
-      = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
-    .form-group.dropdown.compare-form-group.js-compare-from-dropdown
+      .compare-switch-container
+        = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+    .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
       .input-group.inline-input-group
         %span.input-group-addon from
-        = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence }
+        = hidden_field_tag :from, params[:from]
+        = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+          .dropdown-toggle-text= params[:from] || 'Select branch/tag'
       = render "ref_dropdown"
-    = "..."
-    .form-group.dropdown.compare-form-group.js-compare-to-dropdown
+    .compare-ellipsis.inline ...
+    .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
       .input-group.inline-input-group
         %span.input-group-addon to
-        = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence }
+        = hidden_field_tag :to, params[:to]
+        = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+          .dropdown-toggle-text= params[:to] || 'Select branch/tag'
       = render "ref_dropdown"
     &nbsp;
     = button_tag "Compare", class: "btn btn-create commits-compare-btn"
diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml
index c604c6d0135111125f2f2464118fb4a1caf91c7c..05fb37cdc0f526d48daad2d8e41b4e4109ada6c9 100644
--- a/app/views/projects/compare/_ref_dropdown.html.haml
+++ b/app/views/projects/compare/_ref_dropdown.html.haml
@@ -1,4 +1,5 @@
 .dropdown-menu.dropdown-menu-selectable
-  = dropdown_title "Select branch/tag"
+  = dropdown_title "Select Git revision"
+  = dropdown_filter "Filter by Git revision"
   = dropdown_content
   = dropdown_loading
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index e9ff8e90dd56989b1f6577fd0ab6307cedbe5e64..45be6581cfc9ccb1a45bbab9c438898dd0769a12 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -4,7 +4,7 @@
 
 %div{ class: container_class }
   .sub-header-block
-    Compare branches, tags or commit ranges.
+    Compare Git revisions.
     %br
     Fill input field with commit id like
     %code.label-branch 4eedf23
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..247d612ba6f711edc214cf1600d44f0dbc1bd214
--- /dev/null
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -0,0 +1,63 @@
+- @no_container = true
+- page_title "Cycle Analytics"
+
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js')
+
+= render "projects/pipelines/head"
+
+#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
+
+  .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
+    = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
+    .row
+      .col-sm-3.col-xs-12.svg-container
+        = custom_icon('icon_cycle_analytics_splash')
+      .col-sm-8.col-xs-12.inner-content
+        %h4
+          Introducing Cycle Analytics
+        %p
+          Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+
+        = link_to "Read more",  help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+
+  = icon("spinner spin", "v-show" => "isLoading")
+
+  .wrapper{"v-show" => "!isLoading && !hasError"}
+    .panel.panel-default
+      .panel-heading
+        Pipeline Health
+
+      .content-block
+        .container-fluid
+          .row
+            .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
+              %h3.header {{item.value}}
+              %p.text {{item.title}}
+
+            .col-sm-3.col-xs-12.column
+              .dropdown.inline.js-ca-dropdown
+                %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+                  %span.dropdown-label Last 30 days
+                  %i.fa.fa-chevron-down
+                %ul.dropdown-menu.dropdown-menu-align-right
+                  %li
+                    %a{'href' => "#", 'data-value' => '30'}
+                      Last 30 days
+                  %li
+                    %a{'href' => "#", 'data-value' => '90'}
+                      Last 90 days
+
+    .bordered-box
+      %ul.content-list
+        %li{"v-for" => "item in analytics.stats"}
+          .container-fluid
+            .row
+              .col-xs-8.title-col
+                %p.title
+                  {{item.title}}
+                %p.text
+                  {{item.description}}
+              .col-xs-4.value-col
+                %span
+                  {{item.value}}
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index f7bf3b834ef469da187f4297d8c0a0ae2b54ca14..58a214bdbd15a74d55729e6a42c5bc0a13110b93 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,22 +1,15 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
-  .pull-right
-    - actions = deployment.manual_actions
-    - if actions.present?
-      .inline
-        .dropdown
-          %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
-            = icon("play")
-            %b.caret
-          %ul.dropdown-menu.dropdown-menu-align-right
-            - actions.each do |action|
-              %li
-                = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
-                  = icon("play")
-                  %span= action.name.humanize
+- if can?(current_user, :create_deployment, deployment)
+  - actions = deployment.manual_actions
+  - if actions.present?
+    .inline
+      .dropdown
+        %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+          = custom_icon('icon_play')
+          = icon('caret-down')
+        %ul.dropdown-menu.dropdown-menu-align-right
+          - actions.each do |action|
+            %li
+              = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
+                = custom_icon('icon_play')
+                %span= action.name.humanize
 
-    - if local_assigns.fetch(:allow_rollback, false)
-      = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
-        - if deployment.last?
-          Re-deploy
-        - else
-          Rollback
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 28813babd7be6b54c0245e5cea545cd2fff47073..ff250eeca50711175b0009e3683423fcc6e70cac 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -3,7 +3,7 @@
     .icon-container
       = deployment.tag? ? icon('tag') : icon('code-fork')
     = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name"
-  .icon-container
+  .icon-container.commit-icon
     = custom_icon("icon_commit")
   = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
 
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index cd95841ca5a9fb23c4d97bec62518d4f9aea9b63..9238f232c7eab3d03a3bf395611a23f60b7374d7 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -5,14 +5,18 @@
   %td
     = render 'projects/deployments/commit', deployment: deployment
 
-  %td
+  %td.build-column
     - if deployment.deployable
-      = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
-        = user_avatar(user: deployment.user, size: 20)
+      = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
         = "#{deployment.deployable.name} (##{deployment.deployable.id})"
+      - if deployment.user
+        by
+        = user_avatar(user: deployment.user, size: 20)
 
   %td
     #{time_ago_with_tooltip(deployment.created_at)}
 
-  %td
-    = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
+  %td.hidden-xs
+    .pull-right
+      = render 'projects/deployments/actions', deployment: deployment
+      = render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5941e01c6f1edac7f5cda0fb90d2fb33187e7f69
--- /dev/null
+++ b/app/views/projects/deployments/_rollback.haml
@@ -0,0 +1,6 @@
+- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+  = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
+    - if deployment.last?
+      Re-deploy
+    - else
+      Rollback
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index d37961c4e40d56b09512d7e8f33dfb51def7dfa9..779c8ea0104aaf6fca442bd9ce8a01d8897da879 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -11,7 +11,9 @@
     - elsif diff_file.collapsed?
       - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
       .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
-        This diff is collapsed. Click to expand it.
+        This diff is collapsed.
+        %a.click-to-expand
+          Click to expand it.
     - elsif diff_file.diff_lines.length > 0
       - if diff_view == :parallel
         = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 62aff36aadd7f417de3c8d345ec5acb043a30064..067cf595da30afd07ce54f73b3294e4187379184 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,7 +1,6 @@
 - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
+- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
 - diff_files = diffs.diff_files
-- if diff_view == :parallel
-  - fluid_layout true
 
 .content-block.oneline-block.files-changed
   .inline-parallel-buttons
@@ -22,7 +21,7 @@
 - if diff_files.overflow?
   = render 'projects/diffs/warning', diff_files: diff_files
 
-.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
+.files{ data: { can_create_note: can_create_note } }
   - diff_files.each_with_index do |diff_file, index|
     - diff_commit = commit_for_diff(diff_file)
     - blob = diff_file.blob(diff_commit)
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8fbd89100ca5e23c1bd42d4dde9f5f92e5957972..8f4f9ad4a800a1a36ce07e6d08a0f2de7335d5bd 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,15 +5,13 @@
     - unless diff_file.submodule?
       .file-actions.hidden-xs
         - if blob_text_viewable?(blob)
-          = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
+          = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
             = icon('comment')
           \
-
           - if editable_diff?(diff_file)
-            = edit_blob_link(@merge_request.source_project,
-                @merge_request.source_branch, diff_file.new_path,
-                from_merge_request_id: @merge_request.id,
-                skip_visible_check: true)
+            - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+            = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
+                             blob: blob, link_opts: link_opts)
 
         = view_file_btn(diff_commit.id, diff_file.new_path, project)
 
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 95a2772fd0bc5f4f735cab58c66508813efe8e43..d3ed8e1bf3897cb701f575d0614a95e7e32a6cec 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -1,3 +1,4 @@
+%i.fa.diff-toggle-caret.fa-fw
 - if defined?(blob) && blob && diff_file.submodule?
   %span
     = icon('archive fw')
@@ -20,6 +21,8 @@
       - if diff_file.deleted_file
         deleted
 
+  = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard')
+
   - if diff_file.mode_changed?
     %small
       = "#{diff_file.a_mode} → #{diff_file.b_mode}"
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 2d6a370b8482b4a1e1b916f3c10934c5278776c8..a3e4b5b777e5d87ecd62c071c5ebaaf95b67d51c 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,6 +1,7 @@
+- email = local_assigns.fetch(:email, false)
 - plain = local_assigns.fetch(:plain, false)
 - type = line.type
-- line_code = diff_file.line_code(line) unless plain
+- line_code = diff_file.line_code(line)
 %tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
   - case type
   - when 'match'
@@ -22,4 +23,15 @@
         = link_text
       - else
         %a{href: "##{line_code}", data: { linenumber: link_text }}
-    %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type)
+    %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
+      - if email
+        %pre= diff_line_content(line.text)
+      - 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
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 28aad3f472554467ef8696d8dd31aaa6cb27d054..78aa9fb73919f23973aad956b43d2d6881b26473 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
 / Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
+%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
   %table
     - last_line = 0
     - diff_file.parallel_diff_lines.each do |line|
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index ab5463ba89d791e65acb4ca135c3fe8da4bb19d1..f1d2d4bf2689d3cb3245ca9d4a68d14e2addeaf3 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -5,15 +5,12 @@
 
 %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
   - last_line = 0
-  - diff_file.highlighted_diff_lines.each do |line|
-    - last_line = line.new_pos
-    = render "projects/diffs/line", line: line, diff_file: diff_file
-
-    - unless @diff_notes_disabled
-      - line_code = diff_file.line_code(line)
-      - discussion = @grouped_diff_discussions[line_code] if line_code
-      - if discussion
-        = render "discussions/diff_discussion", discussion: discussion
+  - discussions = @grouped_diff_discussions unless @diff_notes_disabled
+  = render partial: "projects/diffs/line",
+    collection: diff_file.highlighted_diff_lines,
+    as: :line,
+    locals: { diff_file: diff_file, discussions: discussions }
 
+  - last_line = diff_file.highlighted_diff_lines.last.new_pos
   - if !diff_file.new_file && last_line > 0
     = diff_match_line last_line, last_line, bottom: true
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b282aa52b25d0a6eadcbd7b1ac1d05062f859f5c..0aa8801c2d8982e35fad17ab5ea38eba30872c42 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -44,51 +44,73 @@
         %hr
         %fieldset.features.append-bottom-0
           %h5.prepend-top-0
-            Features
-          .form-group
-            .checkbox
-              = f.label :issues_enabled do
-                = f.check_box :issues_enabled
-                %strong Issues
-                %br
-                %span.descr Lightweight issue tracking system for this project
-          .form-group
-            .checkbox
-              = f.label :merge_requests_enabled do
-                = f.check_box :merge_requests_enabled
-                %strong Merge Requests
-                %br
-                %span.descr Submit changes to be merged upstream
-          .form-group
-            .checkbox
-              = f.label :builds_enabled do
-                = f.check_box :builds_enabled
-                %strong Builds
-                %br
-                %span.descr Test and deploy your changes before merge
-          .form-group
+            Feature Visibility
+
+          = f.fields_for :project_feature do |feature_fields|
+            .form_group.prepend-top-20
+              .row
+                .col-md-9
+                  = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+                  %span.help-block Push files to be stored in this project
+                .col-md-3.js-repo-access-level
+                  = project_feature_access_select(:repository_access_level)
+
+                .col-sm-12
+                  .row
+                    .col-md-9.project-feature-nested
+                      = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+                      %span.help-block Submit changes to be merged upstream
+                    .col-md-3
+                      = project_feature_access_select(:merge_requests_access_level)
+
+                  .row
+                    .col-md-9.project-feature-nested
+                      = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+                      %span.help-block Submit, test and deploy your changes before merge
+                    .col-md-3
+                      = project_feature_access_select(:builds_access_level)
+
+              .row
+                .col-md-9
+                  = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+                  %span.help-block Share code pastes with others out of Git repository
+                .col-md-3
+                  = project_feature_access_select(:snippets_access_level)
+
+              .row
+                .col-md-9
+                  = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+                  %span.help-block Lightweight issue tracking system for this project
+                .col-md-3
+                  = project_feature_access_select(:issues_access_level)
+
+              .row
+                .col-md-9
+                  = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+                  %span.help-block Pages for project documentation
+                .col-md-3
+                  = project_feature_access_select(:wiki_access_level)
+
+          - if Gitlab.config.lfs.enabled && current_user.admin?
             .checkbox
-              = f.label :wiki_enabled do
-                = f.check_box :wiki_enabled
-                %strong Wiki
+              = f.label :lfs_enabled do
+                = f.check_box :lfs_enabled
+                %strong LFS
                 %br
-                %span.descr Pages for project documentation
-          .form-group
+                %span.descr
+                  Git Large File Storage
+                  = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+
+        - if Gitlab.config.registry.enabled
+          .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
             .checkbox
-              = f.label :snippets_enabled do
-                = f.check_box :snippets_enabled
-                %strong Snippets
+              = f.label :container_registry_enabled do
+                = f.check_box :container_registry_enabled
+                %strong Container Registry
                 %br
-                %span.descr Share code pastes with others out of git repository
-          - if Gitlab.config.registry.enabled
-            .form-group
-              .checkbox
-                = f.label :container_registry_enabled do
-                  = f.check_box :container_registry_enabled
-                  %strong Container Registry
-                  %br
-                  %span.descr Enable Container Registry for this repository
-        %hr
+                %span.descr Enable Container Registry for this project
+                = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
+
         = render 'merge_request_settings', f: f
         %hr
         %fieldset.features.append-bottom-default
@@ -96,7 +118,8 @@
             Project avatar
           .form-group
             - if @project.avatar?
-              = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+              .avatar-container.s160
+                = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
             %p.light
               - if @project.avatar_in_git
                 Project avatar in repository: #{ @project.avatar_in_git }
@@ -158,6 +181,7 @@
           %ul
             %li Build traces and artifacts
             %li LFS objects
+            %li Container registry images
   %hr
   - if can? current_user, :archive_project, @project
     .row.prepend-top-default
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 636beb73ec20fa2b3ca2118ddbb9ec73296e7994..7a39064adc5a1d97439e32793f002c331559b69e 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -23,6 +23,8 @@
       or a
       = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
       to this project.
+    %p
+      You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
 
 - if can?(current_user, :push_code, @project)
   %div{ class: container_class }
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 36a6162a5a862e33b77518f17908b20c90253190..b75d5df4150995dc49a9ad0d5cf77b1584236f11 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -4,10 +4,17 @@
   %td
     = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
 
-  %td
+  %td.deployment-column
     - if last_deployment
-      = user_avatar(user: last_deployment.user, size: 20)
-      %strong ##{last_deployment.id}
+      %span ##{last_deployment.iid} 
+      - if last_deployment.user
+        by
+        = user_avatar(user: last_deployment.user, size: 20)
+
+  %td
+    - if last_deployment && last_deployment.deployable
+      = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
+        = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
 
   %td
     - if last_deployment
@@ -20,5 +27,9 @@
     - if last_deployment
       #{time_ago_with_tooltip(last_deployment.created_at)}
 
-  %td
-    = render 'projects/deployments/actions', deployment: last_deployment
+  %td.hidden-xs
+    .pull-right
+      = render 'projects/environments/external_url', environment: environment
+      = render 'projects/deployments/actions', deployment: last_deployment
+      = render 'projects/environments/stop', environment: environment
+      = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4c8fe1c271b6d6cf7dec025673647d10cda0b46c
--- /dev/null
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -0,0 +1,3 @@
+- if environment.external_url && can?(current_user, :read_environment, environment)
+  = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+    = icon('external-link')
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..69848123c170e950bdb5bb7e48f45b70df913688
--- /dev/null
+++ b/app/views/projects/environments/_stop.html.haml
@@ -0,0 +1,5 @@
+- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+  .inline
+    = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
+      class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
+      = icon('stop', class: 'stop-env-icon')
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
index 6d1bdb9320f302ac72c4dd1c203eb2284f077a4b..3871165763c400de7bf4bb6f8f5b7ea394589829 100644
--- a/app/views/projects/environments/edit.html.haml
+++ b/app/views/projects/environments/edit.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
 - page_title "Edit", @environment.name, "Environments"
+= render "projects/pipelines/head"
 
-%h3.page-title
-  Edit environment
-%hr
-= render 'form'
+%div{ class: container_class }
+  %h3.page-title
+    Edit environment
+  %hr
+  = render 'form'
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index b3eb5b0011a9617efc1b7252764c89bd429e955f..8f555afcf11d975bb4fd978493e86a4ffc705a98 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,31 +3,46 @@
 = render "projects/pipelines/head"
 
 %div{ class: container_class }
-  - if can?(current_user, :create_environment, @project) && !@environments.blank?
-    .top-area
+  .top-area
+    %ul.nav-links
+      %li{class: ('active' if @scope.nil?)}
+        = link_to project_environments_path(@project) do
+          Available
+          %span.badge.js-available-environments-count
+            = number_with_delimiter(@all_environments.available.count)
+
+      %li{class: ('active' if @scope == 'stopped')}
+        = link_to project_environments_path(@project, scope: :stopped) do
+          Stopped
+          %span.badge.js-stopped-environments-count
+            = number_with_delimiter(@all_environments.stopped.count)
+
+    - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
       .nav-controls
         = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
           New environment
 
-  - if @environments.blank?
-    .blank-state.blank-state-no-icon
-      %h2.blank-state-title
-        You don't have any environments right now.
-      %p.blank-state-text
-        Environments are places where code gets deployed, such as staging or production.
-        %br
-        = succeed "." do
-          = link_to "Read more about environments", help_page_path("ci/environments")
-      - if can?(current_user, :create_environment, @project)
-        = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
-          New environment
-  - else
-    .table-holder
-      %table.table.builds.environments
-        %tbody
-          %th Environment
-          %th Last Deployment
-          %th Commit
-          %th
-          %th
-        = render @environments
+  .environments-container
+    - if @all_environments.blank?
+      .blank-state.blank-state-no-icon
+        %h2.blank-state-title
+          You don't have any environments right now.
+        %p.blank-state-text
+          Environments are places where code gets deployed, such as staging or production.
+          %br
+          = succeed "." do
+            = link_to "Read more about environments", help_page_path("ci/environments")
+        - if can?(current_user, :create_environment, @project)
+          = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+            New environment
+    - else
+      .table-holder
+        %table.table.ci-table.environments
+          %tbody
+            %th Environment
+            %th Last Deployment
+            %th Build
+            %th Commit
+            %th
+            %th.hidden-xs
+          = render @environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index e51667ade2dbb1198fcc33e931c1ff287324f0ba..24638c77cbb0dfc864ac218de44940e8cd5ff06e 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
 - page_title 'New Environment'
+= render "projects/pipelines/head"
 
-%h3.page-title
-  New environment
-%hr
-= render 'form'
+%div{ class: container_class }
+  %h3.page-title
+    New environment
+  %hr
+  = render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 8f8c1c4ce22c09b11b9ad4fde9b7ec5a91d28042..bcac73d3698985b012c64f4c97da3839a76824eb 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -3,35 +3,38 @@
 = render "projects/pipelines/head"
 
 %div{ class: container_class }
-  .top-area
+  .top-area.adjust
     .col-md-9
       %h3.page-title= @environment.name.capitalize
     .col-md-3
       .nav-controls
+        = render 'projects/environments/external_url', environment: @environment
         - if can?(current_user, :update_environment, @environment)
           = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
-          = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
+        - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+          = 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
 
-  - if @deployments.blank?
-    .blank-state.blank-state-no-icon
-      %h2.blank-state-title
-        You don't have any deployments right now.
-      %p.blank-state-text
-        Define environments in the deploy stage(s) in
-        %code .gitlab-ci.yml
-        to track deployments here.
-      = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
-  - else
-    .table-holder
-      %table.table.builds.environments
-        %thead
-          %tr
-            %th ID
-            %th Commit
-            %th Build
-            %th
-            %th
+  .deployments-container
+    - if @deployments.blank?
+      .blank-state.blank-state-no-icon
+        %h2.blank-state-title
+          You don't have any deployments right now.
+        %p.blank-state-text
+          Define environments in the deploy stage(s) in
+          %code .gitlab-ci.yml
+          to track deployments here.
+        = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+    - else
+      .table-holder
+        %table.table.ci-table.environments
+          %thead
+            %tr
+              %th ID
+              %th Commit
+              %th Build
+              %th
+              %th.hidden-xs
 
-        = render @deployments
+          = render @deployments
 
-    = paginate @deployments, theme: 'gitlab'
+      = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index a1d79bdabda0b2f0fc76e2beb4ba76f6b4b21673..abf4f697f867732b3801af701c519f9cd9ac28ae 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -15,7 +15,7 @@
           = sort_options_hash[@sort]
         - else
           = sort_title_recently_created
-        %b.caret
+        = icon('caret-down')
       %ul.dropdown-menu.dropdown-menu-align-right
         %li
           - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
@@ -32,11 +32,11 @@
       - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
         = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
           = custom_icon('icon_fork')
-          Fork
+          %span Fork
       - else
         = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
           = custom_icon('icon_fork')
-          Fork
+          %span Fork
 
 
 = render 'projects', projects: @forks
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 331dc1fcc29f1165b7bfbadaa039d8ff9fbbc415..80fe6be49b0989b7592c5f3b1cd58092b46fd4b9 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -62,5 +62,3 @@
     %td.coverage
       - if generic_commit_status.try(:coverage)
         #{generic_commit_status.coverage}%
-
-  %td
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..1c457244a7ade130562e47bc99e465c8f0aaa35d
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -0,0 +1,10 @@
+%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
+  - if subject.target_url
+    = link_to subject.target_url do
+      %span.ci-status-icon
+        = ci_icon_for_status(subject.status)
+      %span.ci-status-text= subject.name
+  - else
+    %span.ci-status-icon
+      = ci_icon_for_status(subject.status)
+    %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 45e51389c00f9e3f4f141e098631ff333a8e04ca..1a62a6a809c952caf49abcfc00a6e6830b7e9e19 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,16 +1,19 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
+= 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_tag('lib/chart.js')
-      = page_specific_javascript_tag('graphs/graphs_bundle.js')
-    = 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.builds_enabled?
-      = nav_link(action: :ci) do
-        = link_to ci_namespace_project_graph_path do
-          Continuous Integration
+        - content_for :page_specific_javascripts do
+          = page_specific_javascript_tag('lib/chart.js')
+          = page_specific_javascript_tag('graphs/graphs_bundle.js')
+        = 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/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28fab5c5eb35323768e9529ae79d11a..1b0dbbb8111aaebf8eee4dfd145e18d1957471fc 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -8,15 +8,22 @@
   .col-lg-9
     %h5.prepend-top-0
       Set a group to share
-    = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do
+    = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
       .form-group
         = label_tag :link_group_id, "Group", class: "label-light"
-        = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+        = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
       .form-group
         = label_tag :link_group_access, "Max access level", class: "label-light"
         .select-wrapper
           = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
-          %span.caret
+          = icon('caret-down')
+      .form-group
+        = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+        .clearable-input
+          = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+          %i.clear-icon.js-clear-input
+        .help-block
+          On this date, all users in the group will automatically lose access to this project.
       = submit_tag "Share", class: "btn btn-create"
   .col-lg-9.col-lg-offset-3
     %hr
@@ -35,6 +42,10 @@
                 = group.name
               %br
               up to #{group_link.human_access}
+              - if group_link.expires?
+                ·
+                %span{ class: ('text-warning' if group_link.expires_soon?) }
+                  expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
             .pull-right
               = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
                 %span.sr-only disable sharing
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
new file mode 100644
index 0000000000000000000000000000000000000000..af9a5b190600c0bf5f4fb93c0260d4456003d5ea
--- /dev/null
+++ b/app/views/projects/group_links/update.js.haml
@@ -0,0 +1,3 @@
+:plain
+  var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
+  $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 3fcf1692e09e9068d1e1f50a39a0c297b23bfaec..ceabe2eab3d50002cddbaf17e94b5c90e273f15c 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
     .col-md-8.col-lg-7
       %strong.light-header= hook.url
       %div
-        - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
+        - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
           - if hook.send(trigger)
             %span.label.label-gray.deploy-project-label= trigger.titleize
     .col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 4d8ee562e6a29565e7c48ffc5bcca779dd69680c..c52b38606369cfa5c2191a0a097bd2eabc825062 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Import in progress"
+- page_title @project.forked? ? "Forking in progress" : "Import in progress"
 .save-project-loader
   .center
     %h2
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 60b45115b73bc69746ba914d9d7cd7ca25431d5b..4825820c4d9d7ad586b80173dae0c14a910adc30 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,25 +1,33 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
-    - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
-      = nav_link(controller: :issues) do
-        = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
-          %span
-            Issues
+= 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) }
+        - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+          = nav_link(controller: :issues) do
+            = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+              %span
+                Issues
 
-    - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
-      = nav_link(controller: :merge_requests) do
-        = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
-          %span
-            Merge Requests
+          = nav_link(controller: :boards) do
+            = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do
+              %span
+                Board
 
-    - if project_nav_tab? :labels
-      = nav_link(controller: :labels) do
-        = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
-          %span
-            Labels
+        - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+          = nav_link(controller: :merge_requests) do
+            = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+              %span
+                Merge Requests
 
-    - if project_nav_tab? :milestones
-      = nav_link(controller: :milestones) do
-        = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
-          %span
-            Milestones
+        - if project_nav_tab? :labels
+          = nav_link(controller: :labels) do
+            = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+              %span
+                Labels
+
+        - if project_nav_tab? :milestones
+          = nav_link(controller: :milestones) do
+            = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+              %span
+                Milestones
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 79b1481986579cf5545e4de8fdb5b8ac88b5e1da..c80210d6ff46b47fa36a02b310557f5211aa278c 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,7 +1,7 @@
 %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
-  - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+  - if @bulk_edit
     .issue-check
-      = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+      = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
 
   .issue-title.title
     %span.issue-title-text
@@ -29,7 +29,7 @@
 
       - note_count = issue.notes.user.count
       %li
-        = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
+        = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
           = icon('comments')
           = note_count
 
@@ -50,7 +50,7 @@
     - if issue.labels.any?
       &nbsp;
       - issue.labels.each do |label|
-        = link_to_label(label, project: issue.project)
+        = link_to_label(label, subject: issue.project)
     - if issue.tasks?
       &nbsp;
       %span.task-status
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
index 72669372497dc53193d858368bc43d13f2153db4..d2038a2be68ae07ee389179613c7bd1e01ac209a 100644
--- a/app/views/projects/issues/_issue_by_email.html.haml
+++ b/app/views/projects/issues/_issue_by_email.html.haml
@@ -12,16 +12,23 @@
           Create new issue by email
       .modal-body
         %p
-          Write an email to the below email address. (This is a private email address, so keep it secret.)
+          You can create a new issue inside this project by sending an email to the following email address:
         .email-modal-input-group.input-group
           = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
           .input-group-btn
             = clipboard_button(clipboard_target: '#issue_email')
         %p
-          Send an email to this address to create an issue.
-        %p
-          Use the subject line as the title of your issue.
+          The subject will be used as the title of the new issue, and the message will be the description.
+
+          = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
+          and styling with
+          = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+          are supported.
+
         %p
-          Use the message as the body of your issue (feel free to include some nice
-          = succeed ")." do
-            = link_to "Markdown", help_page_path('markdown', 'markdown')
+          This is a private email address, generated just for you.
+
+          Anyone who gets ahold of it can create issues as if they were you.
+          You should
+          = link_to 'reset it', new_issue_address_namespace_project_path(@project.namespace, @project), class: 'incoming-email-token-reset'
+          if that ever happens.
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index f34f3c0573743d4cb746e3e51399c6d1054907bb..a4b752ad86ddcdf9a86d05b8fb3b322ae5698983 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,5 +1,5 @@
-%ul.content-list.issues-list
-  = render @issues
+%ul.content-list.issues-list.issuable-list
+  = render partial: "projects/issues/issue", collection: @issues
   - if @issues.blank?
     %li
       .nothing-here-block No issues to show
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d80753718533e3738c05817d844417607c8ebfd2..31d3ec2327611d50c0ed01c6252ad8b118417bf5 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,7 +1,7 @@
 - if @merge_requests.any?
   %h2.merge-requests-title
     = pluralize(@merge_requests.count, 'Related Merge Request')
-  %ul.unstyled-list
+  %ul.unstyled-list.related-merge-requests
     - has_any_ci = @merge_requests.any?(&:pipeline)
     - @merge_requests.each do |merge_request|
       %li
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 24749699c6d3be5eb09f3280558f7625a3b295e3..c56b6cc11f5e40ba5798356779f7cc39c5ed8f2a 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,13 +1,12 @@
 - if can?(current_user, :push_code, @project)
   .pull-right
-    #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+    #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+      = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
+        = icon('spinner spin')
+        Checking branches
       = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
-        method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
-        .checking
-          = icon('spinner spin')
-          Checking branches
-        .available.hide
-          New branch
-        .unavailable.hide
-          = icon('exclamation-triangle')
-          New branch unavailable
+        method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
+        New branch
+      = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do
+        = icon('exclamation-triangle')
+        New branch unavailable
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 6ea9f612d13abe4f6560553acc610fbbf539e4b2..1892ebb512f0fa17a3b70e145b3b891dd1e24dba 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -1,11 +1,11 @@
 - if @related_branches.any?
   %h2.related-branches-title
     = pluralize(@related_branches.count, 'Related Branch')
-  %ul.unstyled-list
+  %ul.unstyled-list.related-merge-requests
     - @related_branches.each do |branch|
       %li
-        - target = @project.repository.find_branch(branch).target
-        - pipeline = @project.pipeline(target.sha, branch) if target
+        - target = @project.repository.find_branch(branch).dereferenced_target
+        - pipeline = @project.pipeline_for(branch, target.sha) if target
         - if pipeline
           %span.related-branch-ci-status
             = render_pipeline_status(pipeline)
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 7cf1923456efe94da56446267b6a82a2e8e265fc..1b7d878c38c0db14eb88a6146d7f048798a3d426 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
 
 %h3.page-title
   Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 36957560de0cf6466c0aea564ee3050d417d8e4d..a0df0db77c5cecc6e5fbc0c6488df3724e08b390 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/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   "#{@project.name} issues"
-  xml.link    href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+  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?
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 1a87045aa60619900f852f0d54c1699c9127be33..c493ff3585b468b31d2a7d0e20375e4f56372b5a 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,11 +1,14 @@
 - @no_container = true
+- @bulk_edit = can?(current_user, :admin_issue, @project)
+
 - page_title "Issues"
 - new_issue_email = @project.new_issue_address(current_user)
-= render "projects/issues/head"
+= content_for :sub_nav do
+  = render "projects/issues/head"
 
 = content_for :meta_tags do
   - if current_user
-    = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
+    = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
 
 %div{ class: (container_class) }
   - if @project.issues.any?
@@ -13,7 +16,7 @@
       = render 'shared/issuable/nav', type: :issues
       .nav-controls
         - if current_user
-          = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
+          = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10' do
             = icon('rss')
             %span.icon-label
               Subscribe
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9f1a046ea74b10e3c9688ecddb5213f60d65c7c0..bd629b5c5197e1fd7e4c404fe3ce128f166c5560 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,4 @@
-- page_title           "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title           "#{@issue.title} (#{@issue.to_reference})", "Issues"
 - page_description     @issue.description
 - page_card_attributes @issue.card_attributes
 
@@ -22,9 +22,9 @@
   - 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{ data: { toggle: "dropdown" } }
-          %span.caret
+        %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)
@@ -53,14 +53,14 @@
 
 
 .issue-details.issuable-details
-  .detail-page-description.content-block
+  .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
     %h2.title
-      = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
+      = markdown_field(@issue, :title)
     - if @issue.description.present?
       .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
         .wiki
           = preserve do
-            = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
+            = markdown_field(@issue, :description)
         %textarea.hidden.js-task-list-field
           = @issue.description
     = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
deleted file mode 100644
index 73c6f2a046c5f18115c0527a6686b4c3a830018b..0000000000000000000000000000000000000000
--- a/app/views/projects/labels/_label.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-- label_css_id = dom_id(label)
-%li{id: label_css_id, data: { id: label.id } }
-  = render "shared/label_row", label: label
-
-  .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
-    %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
-      Options
-      %span.caret
-    .dropdown-menu.dropdown-menu-align-right
-      %ul
-        %li
-          = link_to_label(label, type: :merge_request) do
-            = pluralize label.open_merge_requests_count, 'merge request'
-        %li
-          = link_to_label(label) do
-            = pluralize label.open_issues_count(current_user), 'open issue'
-        - if current_user
-          %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
-            %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
-              %span= label_subscription_toggle_button_text(label)
-        - if can? current_user, :admin_label, @project
-          %li
-            = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
-          %li
-            = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
-
-  .pull-right.hidden-xs.hidden-sm.hidden-md
-    = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
-      = pluralize label.open_merge_requests_count, 'merge request'
-    = link_to_label(label, css_class: 'btn btn-transparent btn-action') do
-      = pluralize label.open_issues_count(current_user), 'open issue'
-
-    - if current_user
-      .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
-        %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
-          %span.sr-only= label_subscription_toggle_button_text(label)
-          = icon('eye', class: 'label-subscribe-button-icon')
-          = icon('spinner spin', class: 'label-subscribe-button-loading')
-
-    - if can? current_user, :admin_label, @project
-      = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
-        %span.sr-only Edit
-        = icon('pencil-square-o')
-      = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
-        %span.sr-only Delete
-        = icon('trash-o')
-
-  - if current_user
-    :javascript
-      new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
index d59563b122aefb78357d98ec2cb153f0ef91b5ca..8d09e2bda114a2780f6a6c7f50a336bd2afbfdb6 100644
--- a/app/views/projects/labels/destroy.js.haml
+++ b/app/views/projects/labels/destroy.js.haml
@@ -1,2 +1,2 @@
-- if @project.labels.size == 0
+- if @labels.empty?
   $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 6901ba13ab70e745531cb37c424a713769ef6921..a80a07b52e61d0dcec5c7af5632ecfa4c64d780e 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
 - page_title "Edit", @label.name, "Labels"
+= render "projects/issues/head"
 
-%h3.page-title
-  Edit Label
-%hr
-= render 'form'
+%div{ class: container_class }
+  %h3.page-title
+    Edit Label
+  %hr
+  = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index db66a0edbd8f399469c2464e40cfd375a7c3c7ee..05a8475dcd68c22cb6364476874de91f701d940d 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -16,21 +16,22 @@
   .labels
     - if can?(current_user, :admin_label, @project)
       -# Only show it in the first page
-      - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
+      - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
       .prioritized-labels{ class: ('hide' if hide) }
         %h5 Prioritized Labels
         %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
           %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
           - if @prioritized_labels.present?
-            = render @prioritized_labels
+            = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+
     .other-labels
       - if can?(current_user, :admin_label, @project)
         %h5{ class: ('hide' if hide) } Other Labels
-      - if @labels.present?
-        %ul.content-list.manage-labels-list.js-other-labels
-          = render @labels
+      %ul.content-list.manage-labels-list.js-other-labels
+        - if @labels.present?
+          = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
         = paginate @labels, theme: 'gitlab'
-      - else
+      - if @labels.blank?
         .nothing-here-block
           - if can?(current_user, :admin_label, @project)
             Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 49ddf9016192d95aa7d10e45090c89b2a41eb511..f0d9be744d1438952059a0d00198dbe604f335eb 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,6 +1,9 @@
+- @no_container = true
 - page_title "New Label"
+= render "projects/issues/head"
 
-%h3.page-title
-  New Label
-%hr
-= render 'form'
+%div{ class: container_class }
+  %h3.page-title
+    New Label
+  %hr
+  = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 53dd300c35c081945b6b4e2097bd04ba1fdfae08..cfb44bd206cd0abe8e511644610b334e9287de40 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -2,7 +2,10 @@
   - if can?(current_user, :update_merge_request, @merge_request)
     - if @merge_request.open?
       = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
-    - if @merge_request.closed?
+    - if @merge_request.reopenable?
       = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
+  %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
+    %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
+      {{ buttonText }}
 
 #notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 5029b365f934e193de54d968f3f1046548d29cc9..12408068834deb62622037caa48fa91dbaacb2db 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,8 @@
 %li{ class: mr_css_classes(merge_request) }
+  - if @bulk_edit
+    .issue-check
+      = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
+
   .merge-request-title.title
     %span.merge-request-title-text
       = link_to merge_request.title, merge_request_path(merge_request)
@@ -37,7 +41,7 @@
 
       - note_count = merge_request.mr_and_commit_notes.user.count
       %li
-        = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
+        = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
           = icon('comments')
           = note_count
 
@@ -58,7 +62,7 @@
     - if merge_request.labels.any?
       &nbsp;
       - merge_request.labels.each do |label|
-        = link_to_label(label, project: merge_request.project, type: 'merge_request')
+        = link_to_label(label, subject: merge_request.project, type: :merge_request)
     - if merge_request.tasks?
       &nbsp;
       %span.task-status
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 446887774a4e343f223bfc7533de3bde36fb5dc1..fe82f751f53c52c3664f173d03c4d0e4b4432a48 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-%ul.content-list.mr-list
+%ul.content-list.mr-list.issuable-list
   = render @merge_requests
   - if @merge_requests.blank?
     %li
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index de39964fca83550fc266b67cccdf5a070ca819fc..466ec1475d8574d4a0e1be716538416345696789 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -65,19 +65,6 @@
 
   - if @merge_request.errors.any?
     = form_errors(@merge_request)
-  - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
-    .light-well.append-bottom-default
-      .center
-        %h4
-          There isn't anything to merge.
-        %p.slead
-          - if @merge_request.source_branch == @merge_request.target_branch
-            You'll need to use different branch names to get a valid comparison.
-          - else
-            %span.label-branch #{@merge_request.source_branch}
-            and
-            %span.label-branch #{@merge_request.target_branch}
-            are the same.
   = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
 
 :javascript
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..74367ab9b7bab824590f8688d7f9fd05f92e9279
--- /dev/null
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -0,0 +1 @@
+= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 598bd743676eb211dec31d0f3da2e0a54fc10f08..9c6f562f7db2f65a70503251bd97dd10cbe5be38 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,45 +18,52 @@
   = f.hidden_field :target_branch
 
 .mr-compare.merge-request
-  %ul.merge-request-tabs.nav-links.no-top.no-bottom
-    %li.commits-tab
-      = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
-        Commits
-        %span.badge= @commits.size
-    - if @pipeline
-      %li.builds-tab.active
-        = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
-          Builds
-          %span.badge= @statuses.size
-    %li.diffs-tab.active
-      = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
-        Changes
-        %span.badge= @diffs.real_size
+  - if @commits.empty?
+    .commits-empty
+      %h4
+        There are no commits yet.
+      = custom_icon ('illustration_no_commits')
+  - else
+    %ul.merge-request-tabs.nav-links.no-top.no-bottom
+      %li.commits-tab.active
+        = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+          Commits
+          %span.badge= @commits.size
+      - if @pipelines.any?
+        %li.builds-tab
+          = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+            Pipelines
+            %span.badge= @pipelines.size
+        %li.builds-tab
+          = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
+            Builds
+            %span.badge= @statuses.size
+      %li.diffs-tab
+        = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
+          Changes
+          %span.badge= @merge_request.diff_size
 
-  .tab-content
-    #commits.commits.tab-pane
-      = render "projects/merge_requests/show/commits"
-    #diffs.diffs.tab-pane.active
-      - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
-        .alert.alert-danger
-          %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
-          %p To preserve performance the line changes are not shown.
-      - else
-        = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
-    - if @pipeline
-      #builds.builds.tab-pane
-        = render "projects/merge_requests/show/builds"
+    .tab-content
+      #commits.commits.tab-pane.active
+        = render "projects/merge_requests/show/commits"
+      #diffs.diffs.tab-pane
+        - # This tab is always loaded via AJAX
+      - if @pipelines.any?
+        #builds.builds.tab-pane
+          = render "projects/merge_requests/show/builds"
+        #pipelines.pipelines.tab-pane
+          = render "projects/merge_requests/show/pipelines"
+
+  .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
-  merge_request = new MergeRequest({
-    action: 'new',
-    diffs_loaded: true,
-    commits_loaded: true
+  var merge_request = new MergeRequest({
+    action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
+    buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}"
   });
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 269198adf913cacc43c9d92deaa78cff51ab1128..f57abe73977105be9ac6d8fe0eec6dd4b16f4aa3 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,9 +1,8 @@
 - page_title           "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
 - page_description     @merge_request.description
 - page_card_attributes @merge_request.card_attributes
-
-- if diff_view == :parallel
-  - fluid_layout true
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
 
 .merge-request{'data-url' => merge_request_path(@merge_request)}
   = render "projects/merge_requests/show/mr_title"
@@ -14,13 +13,16 @@
       - if @merge_request.open?
         .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
+                Run in IDE (Koding)
             = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
               Check out branch
 
           %span.dropdown.inline.prepend-left-5
             %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
               Download as
-              %span.caret
+              = icon('caret-down')
             %ul.dropdown-menu.dropdown-menu-align-right
               %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
               %li= link_to "Plain Diff",    merge_request_path(@merge_request, format: :diff)
@@ -33,7 +35,9 @@
         - if @merge_request.open? && @merge_request.diverged_from_target_branch?
           %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
 
-    = render "projects/merge_requests/show/how_to_merge"
+    - if @merge_request.source_branch_exists?
+      = render "projects/merge_requests/show/how_to_merge"
+
     = render "projects/merge_requests/widget/show.html.haml"
 
     - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
@@ -43,28 +47,45 @@
           = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
 
     - if @commits_count.nonzero?
-      %ul.merge-request-tabs.nav-links.no-top.no-bottom
-        %li.notes-tab
-          = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
-            Discussion
-            %span.badge= @merge_request.mr_and_commit_notes.user.count
-        %li.commits-tab
-          = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
-            Commits
-            %span.badge= @commits_count
-        - if @pipeline
-          %li.builds-tab
-            = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do
-              Builds
-              %span.badge= @statuses.size
-        %li.diffs-tab
-          = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
-            Changes
-            %span.badge= @merge_request.diff_size
+      .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+        %div{ class: container_class }
+          %ul.merge-request-tabs.nav-links.no-top.no-bottom
+            %li.notes-tab
+              = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
+                Discussion
+                %span.badge= @merge_request.mr_and_commit_notes.user.count
+            - if @merge_request.source_project
+              %li.commits-tab
+                = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+                  Commits
+                  %span.badge= @commits_count
+            - if @pipeline
+              %li.pipelines-tab
+                = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+                  Pipelines
+                  %span.badge= @pipelines.size
+              %li.builds-tab
+                = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
+                  Builds
+                  %span.badge= @statuses.size
+            %li.diffs-tab
+              = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+                Changes
+                %span.badge= @merge_request.diff_size
+            %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
+              %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}"  }
+                .line-resolve-all{ "v-show" => "discussionCount > 0",
+                  ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+                  %span.line-resolve-btn.is-disabled{ type: "button",
+                      ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+                    = render "shared/icons/icon_status_success.svg"
+                  %span.line-resolve-text
+                    {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
+                = render "discussions/jump_to_next"
 
-      .tab-content
+      .tab-content#diff-notes-app
         #notes.notes.tab-pane.voting_notes
-          .content-block.content-block-small.oneline-block
+          .content-block.content-block-small
             = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
 
           .row
@@ -76,6 +97,8 @@
           - # This tab is always loaded via AJAX
         #builds.builds.tab-pane
           - # This tab is always loaded via AJAX
+        #pipelines.pipelines.tab-pane
+          - # This tab is always loaded via AJAX
         #diffs.diffs.tab-pane
           - # This tab is always loaded via AJAX
 
diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/branch_from.html.haml
index 4f90dde6fa801e2f065ac6959f7705e6b4818a2a..3837c4b388de3a64ed741a2c61aad978be028f51 100644
--- a/app/views/projects/merge_requests/branch_from.html.haml
+++ b/app/views/projects/merge_requests/branch_from.html.haml
@@ -1 +1,2 @@
-= commit_to_html(@commit, @source_project, false)
+- if @commit
+  = commit_to_html(@commit, @ref, @source_project)
diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/branch_to.html.haml
index 67a7a6bcec9d993f28f9f2c578797cd95100aaeb..d69b71790a0f4fd3e0259274e2f176c0bf73033d 100644
--- a/app/views/projects/merge_requests/branch_to.html.haml
+++ b/app/views/projects/merge_requests/branch_to.html.haml
@@ -1 +1,2 @@
-= commit_to_html(@commit, @target_project, false)
+- if @commit
+  = commit_to_html(@commit, @ref, @target_project)
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d9f74d2cbfbd4c83af628b4c9e418bf9b2de1db2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -0,0 +1,40 @@
+- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+  = page_specific_javascript_tag('lib/ace.js')
+= render "projects/merge_requests/show/mr_title"
+
+.merge-request-details.issuable-details
+  = render "projects/merge_requests/show/mr_box"
+
+= render 'shared/issuable/sidebar', issuable: @merge_request
+
+#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
+    resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } }
+  .loading{"v-if" => "isLoading"}
+    %i.fa.fa-spinner.fa-spin
+
+  .nothing-here-block{"v-if" => "hasError"}
+    {{conflictsData.errorMessage}}
+
+  = render partial: "projects/merge_requests/conflicts/commit_stats"
+
+  .files-wrapper{"v-if" => "!isLoading && !hasError"}
+    .files
+      .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
+        .file-title
+          %i.fa.fa-fw{":class" => "file.iconClass"}
+          %strong {{file.filePath}}
+          = render partial: 'projects/merge_requests/conflicts/file_actions'
+        .diff-content.diff-wrap-lines
+          .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+            = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
+          .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+            = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
+          %div{"v-show" => "file.resolveMode === 'edit' ||  file.type === 'text-editor'"}
+            = render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
+
+    = render partial: "projects/merge_requests/conflicts/submit_form"
+
+-# Components
+= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5ab3cd96163c3d623fe704ae406c616a813d3fdb
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -0,0 +1,16 @@
+.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
+  .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
+    .btn-group
+      %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
+        Inline
+      %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
+        Side-by-side
+
+  .js-toggle-container
+    .commit-stat-summary
+      Showing
+      %strong.cred {{conflictsCountText}}
+      between
+      %strong {{conflictsData.sourceBranch}}
+      and
+      %strong {{conflictsData.targetBranch}}
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..05af57acf038bfedf4a491024c78fe581218ad6b
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -0,0 +1,12 @@
+.file-actions
+  .btn-group{"v-if" => "file.type === 'text'"}
+    %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
+      '@click' => "onClickResolveModeButton(file, 'interactive')",
+      type: 'button' }
+      Interactive mode
+    %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
+      '@click' => "onClickResolveModeButton(file, 'edit')",
+      type: 'button' }
+      Edit inline
+  %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+    View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6ffaa9ad4d226dce4c99c3722fec9cb1a74a717b
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -0,0 +1,16 @@
+.form-horizontal.resolve-conflicts-form
+  .form-group
+    %label.col-sm-2.control-label{ "for" => "commit-message" }
+      Commit message
+    .col-sm-10
+      .commit-message-container
+        .max-width-marker
+        %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
+  .form-group
+    .col-sm-offset-2.col-sm-10
+      .row
+        .col-xs-6
+          %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+            %span {{commitButtonText}}
+        .col-xs-6.text-right
+          = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3c927d362c28612641a7cc4bda453544b861dde2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -0,0 +1,13 @@
+%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
+  .diff-editor-wrap{ "v-show" => "file.showEditor" }
+    .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
+      .discard-changes-alert
+        Are you sure you want to discard your changes?
+        .discard-actions
+          %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
+          %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
+    .editor-wrap{ ":class" => "classObject" }
+      .loading
+        %i.fa.fa-spinner.fa-spin
+      .editor
+        %pre{ "style" => "height: 350px" }
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f094df7fcaa921e182f09e60771ad447f1c18263
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -0,0 +1,15 @@
+%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
+  %table
+    %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+      %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+        %a {{line.new_line}}
+      %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+        %a {{line.old_line}}
+      %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+        {{{line.richText}}}
+      %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+      %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+      %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+        %strong {{{line.richText}}}
+        %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
+          {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5690bf7419cc1f8899735c2c81aa596d29668e85
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
@@ -0,0 +1,10 @@
+%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
+  %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+  %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
+    %strong {{line.richText}}
+    %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
+      {{line.buttonTitle}}
+  %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+    {{line.lineNumber}}
+  %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+    {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..a8ecdf593934dd7af4c3ec2b346e8656bd832e19
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
@@ -0,0 +1,4 @@
+%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
+  %table
+    %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+      %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index ace275c689b8770e1ae420c6802ecfca86424a32..144b3a9c8c85d59096be1972348ac93f98455dcf 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,4 +1,6 @@
 - @no_container = true
+- @bulk_edit = can?(current_user, :admin_merge_request, @project)
+
 - page_title "Merge Requests"
 = render "projects/issues/head"
 = render 'projects/last_push'
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
index 81de60f116c2cb7a3b641d00432d547a17adf104..808ef7fed27d876911458b94ec8c8a2562f0abec 100644
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -1,2 +1 @@
 = render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
-
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index 0b05785430b10f0d34593668c96679686b130f92..a0e12fb3f38340118a7443fd6b17a9af1c8a6c27 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -3,4 +3,4 @@
   Most recent commits displayed first
 
 %ol#commits-list.list-unstyled
-  = render "projects/commits/commits", project: @merge_request.project
+  = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 013b05628fae0750eb0f720e6e9c948cd1c662d9..99c71e1454a8dea52e53c92c9066ae1ecf5a3523 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,4 +1,5 @@
 - if @merge_request_diff.collected?
+  = render 'projects/merge_requests/show/versions'
   = render "projects/diffs/diffs", diffs: @diffs
 - elsif @merge_request_diff.empty?
   .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
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 b727efaa6a670f4d77abb535ed6e2904903e3dfb..f1d5441f9ddecf78bd75350787ba9f3aea7a612d 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
@@ -12,7 +12,7 @@
         %pre.dark#merge-info-1
           - if @merge_request.for_fork?
             :preserve
-              git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
+              git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch}
               git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
           - else
             :preserve
@@ -47,8 +47,9 @@
             Note that pushing to GitLab requires write access to this repository.
         %p
           %strong Tip:
-          You can also checkout merge requests locally by
-          %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines
+          = 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'
 
 :javascript
   $(function(){
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ebf18f6ac8598be8bdcedec09798ee84c6b9e06a..ed23d06ee5e06b6fa6d16624700eb0cb91321398 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,13 +1,13 @@
 .detail-page-description.content-block
   %h2.title
-    = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
+    = markdown_field(@merge_request, :title)
 
   %div
     - if @merge_request.description.present?
       .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
         .wiki
           = preserve do
-            = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
+            = markdown_field(@merge_request, :description)
         %textarea.hidden.js-task-list-field
           = @merge_request.description
 
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index b24bdf22ceb8783a307fd76796d75204ba458740..e7c5bca6a3723a69e460081fd574af1ffc979b51 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,3 +1,7 @@
+- if @merge_request.closed_without_fork?
+  .alert.alert-danger
+    %p The source project of this merge request has been removed.
+
 .clearfix.detail-page-header
   .issuable-header
     .issuable-status-box.status-box{ class: status_box_class(@merge_request) }
@@ -14,9 +18,9 @@
   - if can?(current_user, :update_merge_request, @merge_request)
     .issuable-actions
       .clearfix.issue-btn-group.dropdown
-        %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
-          %span.caret
+        %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{ class: merge_request_button_visibility(@merge_request, true) }
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..afe3f3430c689f22f332274135cdb5be7e3e19f7
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -0,0 +1 @@
+= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..eab48b78cb3d7e9db20248f1f4c45f28cc358f5f
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -0,0 +1,84 @@
+- if @merge_request_diffs.size > 1
+  .mr-version-controls
+    %div.mr-version-menus-container.content-block
+      Changes between
+      %span.dropdown.inline.mr-version-dropdown
+        %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+          %span
+            - if @merge_request_diff.latest?
+              latest version
+            - else
+              version #{version_index(@merge_request_diff)}
+          = icon('caret-down')
+        .dropdown-menu.dropdown-select.dropdown-menu-selectable
+          .dropdown-title
+            %span Version:
+            %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+              = icon('times', class: 'dropdown-menu-close-icon')
+          .dropdown-content
+            %ul
+              - @merge_request_diffs.each do |merge_request_diff|
+                %li
+                  = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+                    %strong
+                      - if merge_request_diff.latest?
+                        latest version
+                      - else
+                        version #{version_index(merge_request_diff)}
+                    .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+                    %small
+                      #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+                      = time_ago_with_tooltip(merge_request_diff.created_at)
+
+      - if @merge_request_diff.base_commit_sha
+        and
+        %span.dropdown.inline.mr-version-compare-dropdown
+          %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+            %span
+              - if @start_sha
+                version #{version_index(@start_version)}
+              - else
+                #{@merge_request.target_branch}
+            = icon('caret-down')
+          .dropdown-menu.dropdown-select.dropdown-menu-selectable
+            .dropdown-title
+              %span Compared with:
+              %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+                = icon('times', class: 'dropdown-menu-close-icon')
+            .dropdown-content
+              %ul
+                - @comparable_diffs.each do |merge_request_diff|
+                  %li
+                    = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+                      %strong
+                        - if merge_request_diff.latest?
+                          latest version
+                        - else
+                          version #{version_index(merge_request_diff)}
+                      .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+                      %small
+                        = time_ago_with_tooltip(merge_request_diff.created_at)
+                %li
+                  = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+                    %strong
+                      #{@merge_request.target_branch} (base)
+                    .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+
+    - if different_base?(@start_version, @merge_request_diff)
+      .content-block
+        = icon('info-circle')
+        Selected versions have different base commits.
+        Changes will include
+        = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
+          new commits
+        from
+        %code #{@merge_request.target_branch}
+
+    - unless @merge_request_diff.latest? && !@start_sha
+      .comments-disabled-notif.content-block
+        = icon('info-circle')
+        - if @start_sha
+          Comments are disabled because you're comparing two versions of this merge request.
+        - else
+          Comments are disabled because you're viewing an old version of this merge request.
+        = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 494695a03a50e3363efe1f8bf06b2ef01aab6493..a82c846baa709dd737e91755a887b198af6c9c33 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -4,14 +4,15 @@
       .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
         = ci_icon_for_status(status)
         %span
-          CI build
+          Pipeline
+          = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
           = ci_label_for_status(status)
         for
         - commit = @merge_request.diff_head_commit
         = succeed "." do
           = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
         %span.ci-coverage
-        = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
+        = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
 
 - elsif @merge_request.has_ci?
   - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
@@ -43,15 +44,5 @@
       = icon("times-circle")
       Could not connect to the CI server. Please check your settings and try again.
 
-- @merge_request.environments.each do |environment|
-  .mr-widget-heading
-    .ci_widget.ci-success
-      = ci_icon_for_status("success")
-      %span.hidden-sm
-        Deployed to
-        = succeed '.' do
-          = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
-        - external_url = environment.external_url
-        - if external_url
-          = link_to external_url, target: '_blank' do
-            = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+.js-success-icon.hidden
+  = ci_icon_for_status('success')
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 19b5d0ff0664f5b64a8acb04e372fe0ecf725e4b..7794d6d7df2ffadd052c924af87fed2626663b9d 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -6,7 +6,7 @@
       - if @merge_request.merge_event
         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[:delete_source] == 'true')
+    - 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"}.
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index dc18f715f25b25b0b6c9d9d07be1e859262c345c..01314eb37d0b6af597e93ae93a37ab724639a13c 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -1,6 +1,12 @@
 .mr-state-widget
   = render 'projects/merge_requests/widget/heading'
   .mr-widget-body
+    -# After conflicts are resolved, the user is redirected back to the MR page.
+    -# There is a short window before background workers run and GitLab processes
+    -# the new push and commits, during which it will think the conflicts still exist.
+    -# We send this param to get the widget to treat the MR as having no more conflicts.
+    - resolved_conflicts = params[:resolved_conflicts]
+
     - if @project.archived?
       = render 'projects/merge_requests/widget/open/archived'
     - elsif @merge_request.commits.blank?
@@ -9,7 +15,7 @@
       = render 'projects/merge_requests/widget/open/missing_branch'
     - elsif @merge_request.unchecked?
       = render 'projects/merge_requests/widget/open/check'
-    - elsif @merge_request.cannot_be_merged?
+    - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
       = render 'projects/merge_requests/widget/open/conflicts'
     - elsif @merge_request.work_in_progress?
       = render 'projects/merge_requests/widget/open/wip'
@@ -17,9 +23,11 @@
       = render 'projects/merge_requests/widget/open/merge_when_build_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 && @pipeline.failed?
+    - elsif !@merge_request.mergeable_ci_state?
       = render 'projects/merge_requests/widget/open/build_failed'
-    - elsif @merge_request.can_be_merged?
+    - elsif !@merge_request.mergeable_discussions_state?
+      = render 'projects/merge_requests/widget/open/unresolved_discussions'
+    - elsif @merge_request.can_be_merged? || resolved_conflicts
       = render 'projects/merge_requests/widget/open/accept'
 
   - if mr_closes_issues.present?
@@ -29,3 +37,4 @@
         Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
         = succeed '.' do
           != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+        = mr_assign_issues_link
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index d9efe81701f222fb81d25b86c972414b420a5198..608fdf1c5f5eb3d60c1a12291e6ca7f96e03b49e 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -12,6 +12,7 @@
     merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
     check_enable: #{@merge_request.unchecked? ? "true" : "false"},
     ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+    ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
     gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
     ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
     ci_message: {
@@ -23,7 +24,8 @@
       preparing: "{{status}} build",
       normal: "Build {{status}}"
     },
-    builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
+    builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+    pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
   };
 
   if (typeof merge_request_widget !== 'undefined') {
@@ -32,4 +34,4 @@
     merge_request_widget.clearEventListeners();
   }
 
-  merge_request_widget = new MergeRequestWidget(opts);
+  merge_request_widget = new window.gl.MergeRequestWidget(opts);
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 bf2e76f008386a00cb8fa6c1c0a35715ed6cc71b..ce43ca3a2866dce658489c4560435eafed3da5a3 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -12,7 +12,7 @@
               Merge When Build Succeeds
             - unless @project.only_allow_merge_if_build_succeeds?
               = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
-                %span.caret
+                = icon('caret-down')
                 %span.sr-only
                   Select Merge Moment
               %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
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 f000cc38a653a3b34dfa875f42e0fcc27926eac4..af3096f04d97d4c43d93d09b7d437bdc811a87fd 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -3,7 +3,18 @@
   This merge request contains merge conflicts
 
 %p
-  Please resolve these conflicts or
+  Please
+  - if @merge_request.conflicts_can_be_resolved_by?(current_user)
+    - if @merge_request.conflicts_can_be_resolved_in_ui?
+      = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+    - else
+      %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
+        resolve these conflicts locally
+  - else
+    resolve these conflicts
+
+  or
+
   - if @merge_request.can_be_merged_via_command_line_by?(current_user)
     #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
   - else
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
new file mode 100644
index 0000000000000000000000000000000000000000..35d5677ee3775248d1ed5e81ae5753eaa4e92167
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -0,0 +1,6 @@
+%h4
+  = icon('exclamation-triangle')
+  This merge request has unresolved discussions
+
+%p
+  Please resolve these discussions to allow this merge request to be merged.
\ No newline at end of file
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index be682226ab68e2b21436b8399d86cc8a88d2a78a..11f41e75e633750f444f99fd9ed3578ec71e89c5 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,8 +1,12 @@
+- @no_container = true
 - page_title "Edit", @milestone.title, "Milestones"
+= render "projects/issues/head"
 
-%h3.page-title
-  Edit Milestone ##{@milestone.iid}
+%div{ class: container_class }
 
-%hr
+  %h3.page-title
+    Edit Milestone ##{@milestone.iid}
 
-= render "form"
+  %hr
+
+  = render "form"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 7f372b41698bd4f865e5bc2aeb9ea406528308a4..cda093ade819502f923f7bd2b8cd7e1994b95ba3 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,8 +1,11 @@
+- @no_container = true
 - page_title "New Milestone"
+= render "projects/issues/head"
 
-%h3.page-title
-  New Milestone
+%div{ class: container_class }
+  %h3.page-title
+    New Milestone
 
-%hr
+  %hr
 
-= render "form"
+  = render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 73772cc0e323e0cc971513f0b091a0ef8a579332..f9ba77e87b579f9500c31cd3ef1952c07133a517 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,49 +1,52 @@
+- @no_container = true
 - page_title       @milestone.title, "Milestones"
 - page_description @milestone.description
+= render "projects/issues/head"
 
-.detail-page-header
-  .status-box{ class: status_box_class(@milestone) }
-    - if @milestone.closed?
-      Closed
-    - elsif @milestone.expired?
-      Past due
-    - else
-      Open
-  %span.identifier
-    Milestone ##{@milestone.iid}
-  - if @milestone.expires_at
-    %span.creator
-      &middot;
-      = @milestone.expires_at
-  .pull-right
-    - if can?(current_user, :admin_milestone, @project)
-      - if @milestone.active?
-        = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+%div{ class: container_class }
+  .detail-page-header
+    .status-box{ class: status_box_class(@milestone) }
+      - if @milestone.closed?
+        Closed
+      - elsif @milestone.expired?
+        Past due
       - else
-        = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
+        Open
+    %span.identifier
+      Milestone ##{@milestone.iid}
+    - if @milestone.expires_at
+      %span.creator
+        &middot;
+        = @milestone.expires_at
+    .pull-right
+      - if can?(current_user, :admin_milestone, @project)
+        - if @milestone.active?
+          = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+        - else
+          = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
 
-      = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
-        Edit
+        = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
+          Edit
 
-      = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
-        Delete
+        = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
+          Delete
 
-.detail-page-description.milestone-detail
-  %h2.title
-    = markdown escape_once(@milestone.title), pipeline: :single_line
-  %div
-    - if @milestone.description.present?
-      .description
-        .wiki
-          = preserve do
-            = markdown @milestone.description
+  .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
+    %h2.title
+      = markdown_field(@milestone, :title)
+    %div
+      - if @milestone.description.present?
+        .description
+          .wiki
+            = preserve do
+              = markdown_field(@milestone, :description)
 
-- if @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?
-  .alert.alert-success.prepend-top-default
-    %span All issues for this milestone are closed. You may close this milestone now.
+  - if @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?
+    .alert.alert-success.prepend-top-default
+      %span All issues for this milestone are closed. You may close this milestone now.
 
-= render 'shared/milestones/summary', milestone: @milestone, project: @project
-= render 'shared/milestones/tabs', milestone: @milestone
+  = render 'shared/milestones/summary', milestone: @milestone, project: @project
+  = render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b2ece44d9663b7654ec1ef724ef8f303ecb34ac0..d8951e692421633d4695a6cbc681e07c372812d0 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -8,7 +8,7 @@
   .project-network
     .controls
       = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f|
-        = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha'
+        = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha'
         = button_tag class: 'btn btn-success' do
           = icon('search')
         .inline.prepend-left-20
@@ -17,5 +17,6 @@
               = check_box_tag :filter_ref, 1, @options[:filter_ref]
               %span Begin with the selected commit
 
-    .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
-      = spinner nil, true
+    - if @commit
+      .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
+        = spinner nil, true
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index ea4898f2107fa678044c84cb38670bb2ead09170..932603f03b027e568309847938468020408496ba 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -27,6 +27,7 @@
                 - else
                   .input-group-addon.static-namespace
                     #{root_url}#{current_user.username}/
+                  = f.hidden_field :namespace_id, value: current_user.namespace_id
           .form-group.col-xs-12.col-sm-6.project-path
             = f.label :namespace_id, class: 'label-light' do
               %span
@@ -55,15 +56,10 @@
                       = render 'bitbucket_import_modal'
                 %div
                   - if gitlab_import_enabled?
-                    = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+                    = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
                       = icon('gitlab', text: 'GitLab.com')
                     - unless gitlab_import_configured?
                       = render 'gitlab_import_modal'
-                %div
-                  - if gitorious_import_enabled?
-                    = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
-                      %i.icon-gitorious.icon-gitorious-small
-                      Gitorious.org
                 %div
                   - if google_code_import_enabled?
                     = link_to new_import_google_code_path, class: 'btn import_google_code' do
@@ -77,7 +73,7 @@
                     = link_to "#", class: 'btn js-toggle-button import_git' do
                       = icon('git', text: 'Repo by URL')
                 %div{ class: 'import_gitlab_project' }
-                  - if gitlab_project_import_enabled? && current_user.is_admin?
+                  - if gitlab_project_import_enabled?
                     = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
                       = icon('gitlab', text: 'GitLab export')
 
@@ -131,6 +127,11 @@
     }
   });
 
+  $('#new_project').submit(function(){
+    var $path = $('#project_path');
+    $path.val($path.val().trim());
+  });
+
   $('#project_path').keyup(function(){
     if($(this).val().length !=0) {
       $('.btn_import_gitlab_project').attr('disabled', false);
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 7c61ba750fe5e0e0fe14587e0f6c025fcfe84c41..46b402545cddb85d20cc75acf19ddd48da801634 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,6 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f|
+- supports_slash_commands = note_supports_slash_commands?(@note)
+
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
   = hidden_field_tag :view, diff_view
   = hidden_field_tag :line_type
   = note_target_fields(@note)
@@ -10,8 +12,12 @@
   = f.hidden_field :position
 
   = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
-    = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
-    = render 'projects/notes/hints'
+    = render 'projects/zen', f: f,
+      attr: :note,
+      classes: 'note-textarea js-note-text',
+      placeholder: "Write a comment or drag your files here...",
+      supports_slash_commands: supports_slash_commands
+    = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
     .error-alert
 
   .note-form-actions.clearfix
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 25466e7562e04e8bf19d92e1190f6c3fc516e12b..6c14f48d41bcf4265a4c322ea4edeeaaaf6d2c25 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -1,8 +1,15 @@
+- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
 .comment-toolbar.clearfix
   .toolbar-text
     Styling with
-    = link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1
-    is supported
+    = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+    - if supports_slash_commands
+      and
+      = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
+      are
+    - else
+      is
+    supported
   %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
     = icon('file-image-o', class: 'toolbar-button-icon')
-    Attach a file
\ No newline at end of file
+    Attach a file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 71da8ac9d7c1e582cb4ac456c099564dd47678ff..ab719e3890497ed859de3a7590f89d95bfc88de0 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -16,23 +16,52 @@
             commented
           %a{ href: "##{dom_id(note)}" }
             = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
-        .note-actions
-          - access = note_max_access_for_user(note)
-          - if access and not note.system
-            %span.note-role.hidden-xs= access
-          - if current_user and not note.system
-            = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
-              = icon('spinner spin')
-              = icon('smile-o')
-          - if note_editable
-            = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
-              = icon('pencil')
-            = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
-              = icon('trash-o')
+        - unless note.system?
+          .note-actions
+            - access = note_max_access_for_user(note)
+            - if access
+              %span.note-role.hidden-xs= access
+
+            - if note.resolvable?
+              - can_resolve = can?(current_user, :resolve_note, note)
+              %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)}",
+                  "v-show" => "#{can_resolve || note.resolved?}",
+                  "inline-template" => true,
+                  "v-ref:note_#{note.id}" => true }
+
+                .note-action-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",
+                      "v-el:button" => true }
+
+                    = render "shared/icons/icon_status_success.svg"
+
+            - if current_user
+              - if note.emoji_awardable?
+                = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
+                  = icon('spinner spin')
+                  = icon('smile-o', class: 'link-highlight')
+
+              - if note_editable
+                = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
+                  = icon('pencil', class: 'link-highlight')
+                = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+                  = icon('trash-o', class: 'danger-highlight')
       .note-body{class: note_editable ? 'js-task-list-container' : ''}
         .note-text.md
           = preserve do
-            = note.note_html
+            = note.redacted_note_html
           = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
         - if note_editable
           = render 'projects/notes/edit_form', note: note
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 74538a9723e089cf05ac043ca9dbb5abb11c7d3f..00b62a595ff213fe190071003c1868f6cb5fdf06 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -16,7 +16,7 @@
           Please
           = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
           or
-          = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
+          = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
           to post a comment
 
 :javascript
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index d65faf86d4ed4cb8c672ad1020356cc6e2ea4b96..b10dd47709f07e02eb6b6c61345511ac93e804b3 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,19 +1,28 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
-    - if project_nav_tab? :pipelines
-      = nav_link(controller: :pipelines) do
-        = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
-          %span
-            Pipelines
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .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
+            = 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
-        = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
-          %span
-            Builds
+        - if project_nav_tab? :builds
+          = nav_link(controller: %w(builds)) do
+            = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+              %span
+                Builds
 
-    - if project_nav_tab? :environments
-      = nav_link(controller: %w(environments)) do
-        = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
-          %span
-            Environments
+        - if project_nav_tab? :environments
+          = nav_link(controller: %w(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
+              %span
+                Cycle Analytics
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde755c31ecf5bdef79e988a19bae03fb..d288efc546fc63008af85f3e4f258371a37dbd56 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,9 @@
     = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
   - if @pipeline.duration
     in
-    = time_interval_in_words @pipeline.duration
+    = time_interval_in_words(@pipeline.duration)
+  - if @pipeline.queued_duration
+    = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
 
   .pull-right
     = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
@@ -31,7 +33,7 @@
 - if @commit
   .commit-box.content-block
     %h3.commit-title
-      = markdown escape_once(@commit.title), pipeline: :single_line
+      = markdown(@commit.title, pipeline: :single_line)
     - if @commit.description.present?
       %pre.commit-description
-        = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
+        = preserve(markdown(@commit.description, pipeline: :single_line))
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5f466bdbac2c89882907ffcce380028984590142..4bc49072f3577073b606605bd6f5476ad3ac73a1 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -36,23 +36,21 @@
         = link_to ci_lint_path, class: 'btn btn-default' do
           %span CI Lint
 
-  %ul.content-list.pipelines
+  %div.content-list.pipelines
     - stages = @pipelines.stages
     - if @pipelines.blank?
-      %li
+      %div
         .nothing-here-block No pipelines to show
     - else
       .table-holder
-        %table.table.builds
-          %tbody
+        %table.table.ci-table
+          %thead
             %th Status
+            %th Pipeline
             %th Commit
-            - stages.each do |stage|
-              %th.stage
-                %span.has-tooltip{ title: "#{stage.titleize}" }
-                  = stage.titleize
-            %th
+            %th Stages
             %th
+            %th.hidden-xs
           = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
 
       = paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 75943c64276bfd4b0318990caa700c0d46c2daeb..688535ad764f57b4d32171213b9df37229d9b83f 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -1,8 +1,11 @@
+- @no_container = true
 - page_title "Pipeline"
+= render "projects/pipelines/head"
 
-.prepend-top-default
-  - if @commit
-    = render "projects/pipelines/info"
-  %div.block-connector
+%div{ class: container_class }
+  .prepend-top-default
+    - if @commit
+      = render "projects/pipelines/info"
+    %div.block-connector
 
-= render "projects/commit/pipeline", pipeline: @pipeline
+  = render "projects/commit/pipeline", pipeline: @pipeline
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 8c7222bfe3d3122f46264f4f022bf38e4f4e8e84..96221a205024a794dc286a44990a846b5c666cd3 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -5,33 +5,59 @@
     %h4.prepend-top-0
       = page_title
   .col-lg-9
-    %h5.prepend-top-0
-      Pipelines
-    = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f|
+    = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
       %fieldset.builds-feature
         - unless @repository.gitlab_ci_yml
           .form-group
             %p Pipelines need to be configured before you can begin using Continuous Integration.
             = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+            %hr
+        .form-group.append-bottom-default
+          = f.label :runners_token, "Runner token", class: 'label-light'
+          = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+          %p.help-block The secure token used by the Runner to checkout the project
+
+        %hr
         .form-group
-          %p Get recent application code using the following command:
+          %h5.prepend-top-0
+            Git strategy for pipelines
+          %p
+            Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+            = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
           .radio
             = f.label :build_allow_git_fetch_false do
               = f.radio_button :build_allow_git_fetch, 'false'
               %strong git clone
               %br
-              %span.descr Slower but makes sure you have a clean dir before every build
+              %span.descr
+                Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
           .radio
             = f.label :build_allow_git_fetch_true do
               = f.radio_button :build_allow_git_fetch, 'true'
               %strong git fetch
               %br
-              %span.descr Faster
+              %span.descr
+                Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
 
+        %hr
         .form-group
           = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
           = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
-          %p.help-block per build in minutes
+          %p.help-block
+            Per job in minutes. If a job passes this threshold, it will be marked as failed.
+            = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
+
+        %hr
+        .form-group
+          .checkbox
+            = f.label :public_builds do
+              = f.check_box :public_builds
+              %strong Public pipelines
+            .help-block
+              Allow everyone to access pipelines for public and internal projects
+              = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
+
+        %hr
         .form-group
           = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
           .input-group
@@ -39,8 +65,9 @@
             = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
             %span.input-group-addon /
           %p.help-block
-            We will use this regular expression to find test coverage output in build trace.
-            Leave blank if you want to disable this feature
+            A regular expression that will be used to find the test coverage
+            output in the build trace. Leave blank to disable
+            = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
           .bs-callout.bs-callout-info
             %p Below are examples of regex for existing tools:
             %ul
@@ -57,21 +84,9 @@
                 gcovr (C/C++) -
                 %code ^TOTAL.*\s+(\d+\%)$
               %li
-                tap --coverage-report=text-summary (Node.js) -
+                tap --coverage-report=text-summary (NodeJS) -
                 %code ^Statements\s*:\s*([^%]+)
 
-        .form-group
-          .checkbox
-            = f.label :public_builds do
-              = f.check_box :public_builds
-              %strong Public pipelines
-            .help-block Allow everyone to access pipelines for Public and Internal projects
-
-        .form-group.append-bottom-default
-          = f.label :runners_token, "Runners token", class: 'label-light'
-          = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
-          %p.help-block The secure token used to checkout project.
-
         = f.submit 'Save changes', class: "btn btn-save"
 
 %hr
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index e783d8c72c52dd82190f1b21c48844555e7910fb..9738f369a35368d50cbb842893b7b4698066df3b 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -1,7 +1,7 @@
 .panel.panel-default
   .panel-heading
+    Group members with access to
     %strong #{@group.name}
-    group members
     %span.badge= members.size
     - if can?(current_user, :admin_group_member, @group)
       .controls
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d7f5fa965270246888f60fa0ce1c5590e05df7ef
--- /dev/null
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -0,0 +1,7 @@
+.panel.panel-default.project-members-groups
+  .panel-heading
+    Groups with access to
+    %strong #{@project.name}
+    %span.badge= group_links.size
+  %ul.content-list
+    = render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 978c4dfc5ec92af04008bf2241d51b32875d4a9b..79dcd7a6ee9acb501ef63897603233be50dcbd0d 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,18 +1,22 @@
-= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f|
-  .form-group
-    = f.label :user_ids, "People", class: 'control-label'
-    .col-sm-10
-      = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
-      .help-block
+= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
+  .row
+    .col-md-4.col-lg-6
+      = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
+      .help-block.append-bottom-10
         Search for users by name, username, or email, or invite new ones using their email address.
 
-  .form-group
-    = f.label :access_level, "Project Access", class: 'control-label'
-    .col-sm-10
-      = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
-      .help-block
-        Read more about role permissions
-        %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+    .col-md-3.col-lg-2
+      = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+      .help-block.append-bottom-10
+        = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+        about role permissions
 
-  .form-actions
-    = f.submit 'Add users to project', class: "btn btn-create"
+    .col-md-3.col-lg-2
+      .clearable-input
+        = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+        %i.clear-icon.js-clear-input
+      .help-block.append-bottom-10
+        On this date, the user(s) will automatically lose access to this project.
+
+    .col-md-2
+      = f.submit "Add to project", class: "btn btn-create btn-block"
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index b0bfdd235f7b24277909ee78f76705387fe8fe96..c1e894d8f40f1395c5c2dc46800634a21a4c44d9 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,19 +1,7 @@
 .panel.panel-default
   .panel-heading
+    Users with access to
     %strong #{@project.name}
-    project members
-    %span.badge= members.size
-    .controls
-      = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form'  do
-        .form-group
-          = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
-        = button_tag class: 'btn', title: 'Search' do
-          = icon("search")
+    %span.badge= @project_members.total_count
   %ul.content-list
     = render partial: 'shared/members/member', collection: members, as: :member
-
-:javascript
-  $('form.member-search-form').on('submit', function (event) {
-    event.preventDefault();
-    Turbolinks.visit(this.action + '?' + $(this).serialize());
-  });
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496d51c94e62ee659843436b4fc572d3..bdeb704b6daa6a7c01393f5aab0f5778a168c185 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,24 +1,28 @@
 - page_title "Members"
 
 .project-members-page.prepend-top-default
+  %h4.project-members-title.clearfix
+    Members
+    = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
   - if can?(current_user, :admin_project_member, @project)
-    .panel.panel-default
-      .panel-heading
-        Add new user to project
-        .controls
-          = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
-            Import members
-      .panel-body
-        %p.light
-          Users with access to this project are listed below.
-        = render "new_project_member"
+    .project-members-new.append-bottom-default
+      %p.clearfix
+        Add new user to
+        %strong= @project.name
+      = render "new_project_member"
 
-    = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+      = render 'shared/members/requests', membership_source: @project, requesters: @requesters
 
-  = render 'team', members: @project_members
-
-  - if @group
-    = render "group_members", members: @group_members
+  .append-bottom-default.clearfix
+    %h5.member.existing-title
+      Existing users and groups
+    = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form'  do
+      .form-group
+        = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+        %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+          = icon("search")
+  - if @group_links.any?
+    = render 'groups', group_links: @group_links
 
-  - if @project_group_links.any? && @project.allowed_to_share_with_group?
-    = render "shared_group_members"
+  = render 'team', members: @project_members
+  = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef890606e95ce6e022b3b00664d4b2921351..91927181efbeba7fd4145f1a203189c5859e34a6 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
 :plain
-  $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+  var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+  $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
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 d4c6fa247688a86e569c8ae7fbeacbee05be89ab..e95a3b1b4c368810e70e0197d3620826df36e0c9 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -22,16 +22,20 @@
           %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
             Allowed to merge:
           .col-md-10
-            = dropdown_tag('Select',
-                           options: { toggle_class: 'js-allowed-to-merge wide',
-                           data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
+            .merge_access_levels-container
+              = dropdown_tag('Select',
+                             options: { toggle_class: 'js-allowed-to-merge wide',
+                             dropdown_class: 'dropdown-menu-selectable',
+                             data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
         .form-group
           %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
             Allowed to push:
           .col-md-10
-            = dropdown_tag('Select',
-                           options: { toggle_class: 'js-allowed-to-push wide',
-                           data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
+            .push_access_levels-container
+              = dropdown_tag('Select',
+                             options: { toggle_class: 'js-allowed-to-push wide',
+                             dropdown_class: 'dropdown-menu-selectable',
+                             data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
 
     .panel-footer
       = f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0628134b1bb781058db0f243019071a4b6d21fc0..0193800dedfd4015312c767f8e9a6e152d815c7f 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,4 +1,4 @@
-%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
   %td
     = protected_branch.name
     - if @project.root_ref?(protected_branch.name)
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 49dcc9a6ba4ea1df381a37a8504882996329b872..42e9bdbd30eb6cd4511fa9acf9d13e8ed8908c1c 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,4 +1,6 @@
 - page_title "Protected branches"
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js')
 
 .row.prepend-top-default.append-bottom-default
   .col-lg-3
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 8ee2aef0e61d60815e2334eafd6dba52d04244f7..44fa4b6034341a5bc3bb6d4ee08c0937dcfe696d 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -5,8 +5,8 @@
 
   :plain
     var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}");
-    row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
-    row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+    row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
+    row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
 
 - if @more_log_url
   :plain
@@ -16,3 +16,6 @@
       var url = "#{escape_javascript(@more_log_url)}";
       ajaxGet(url);
     }
+
+:plain
+  gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
\ No newline at end of file
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 835398b6f9895ca0ad4251664ecc67807955d5a9..33d5cbff42069036094b23d9ef39901ee25b3700 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,18 +1,20 @@
+- @no_container = true
 - page_title "Edit", @tag.name, "Tags"
 = render "projects/commits/head"
 
-.row-content-block
-  .oneline
-    .title
-      Release notes for tag
-      %strong #{@tag.name}
+%div{ class: container_class }
+  .sub-header-block.no-bottom-space
+    .oneline
+      .title
+        Release notes for tag
+        %strong #{@tag.name}
+
 
-.prepend-top-default
   = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
     = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
       = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
       = render 'projects/notes/hints'
     .error-alert
-    .form-actions.prepend-top-default
+    .prepend-top-default
       = f.submit 'Save changes', class: 'btn btn-save'
       = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
deleted file mode 100644
index 24658319060b7c5f4fd4eb2bc786b3bb2e260eef..0000000000000000000000000000000000000000
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-- ref = ref || nil
-- btn_class = btn_class || ''
-- split_button = split_button || false
-- if split_button == true
-  %span.btn-group{class: btn_class}
-    = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
-      %i.fa.fa-download
-      %span Download zip
-    %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
-      %span.caret
-      %span.sr-only
-        Select Archive Format
-    %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
-      %li
-        = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
-          %i.fa.fa-download
-          %span Download zip
-      %li
-        = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
-          %i.fa.fa-download
-          %span Download tar.gz
-      %li
-        = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
-          %i.fa.fa-download
-          %span Download tar.bz2
-      %li
-        = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do
-          %i.fa.fa-download
-          %span Download tar
-- else
-  %span.btn-group{class: btn_class}
-    = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
-      %i.fa.fa-download
-      %span zip
-    = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do
-      %i.fa.fa-download
-      %span tar.gz
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 43a6fdfd103c7cb3486b4701a592fbea324c9ca8..d9c39fb87b76fcef28313effeb8268f0f38c9d4f 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
       = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
         %code= commit.short_id
       = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
-      = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
+      = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
   %td
     %span.pull-right.cgray
       = time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index c45a9d4f81feb53f83acde06f43f7b8db224fc0a..33a9a96183cb821179c9bcffe959afef5d76ca19 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
     .col-sm-10
       .checkbox
         = f.check_box :active
-        %span.light Paused runners don't accept new builds
+        %span.light Paused Runners don't accept new builds
   .form-group
     = label :run_untagged, 'Run untagged jobs', class: 'control-label'
     .col-sm-10
@@ -33,6 +33,6 @@
       Tags
     .col-sm-10
       = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control'
-      .help-block You can setup jobs to only use runners with specific tags
+      .help-block You can setup jobs to only use Runners with specific tags
   .form-actions
     = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 852258577584a2aa773b79f732976a1f77a19f4d..6e58e5a0c781a0c4a211c0a58b3000026abb6b20 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -15,7 +15,7 @@
     .pull-right
       - if @project_runners.include?(runner)
         - if runner.belongs_to_one_project?
-          = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+          = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
         - else
           - runner_project = @project.runner_projects.find_by(runner_id: runner)
           = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 9fa4127c9484e6384051f7c04b2df7167ee25b07..5afa193357ef7227e16e694fd3862152a0bda895 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,24 +1,26 @@
-%h3 Shared runners
+%h3 Shared Runners
 
 .bs-callout.bs-callout-warning.shared-runners-description
-  - if shared_runners_text.present?
-    = markdown(shared_runners_text, pipeline: 'plain_markdown')
+  - if current_application_settings.shared_runners_text.present?
+    = markdown_field(current_application_settings, :shared_runners_text)
   - else
-    Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com).
+    GitLab Shared Runners execute code of different projects on the same Runner
+    unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
+    on GitLab.com).
   %hr
   - if @project.shared_runners_enabled?
     = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
-      Disable shared runners
+      Disable shared Runners
   - else
     = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do
-      Enable shared runners
+      Enable shared Runners
   &nbsp; for this project
 
 - if @shared_runners_count.zero?
-  This GitLab server does not provide any shared runners yet.
-  Please use specific runners or ask the administrator to create one.
+  This GitLab server does not provide any shared Runners yet.
+  Please use the specific Runners or ask your administrator to create one.
 - else
-  %h4.underlined-title Available shared runners - #{@shared_runners_count}
+  %h4.underlined-title Available shared Runners : #{@shared_runners_count}
   %ul.bordered-list.available-shared-runners
     = render partial: 'runner', collection: @shared_runners, as: :runner
   - if @shared_runners_count > 10
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index d469dda5b81edbda197196245e6739e2d6583847..51b0939564ebc25a0736e1db9d0a08dfddf5e9d1 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,20 +1,20 @@
-%h3 Specific runners
+%h3 Specific Runners
 
 .bs-callout.help-callout
-  %h4 How to setup a new project specific runner
+  %h4 How to setup a specific Runner for a new project
 
   %ol
     %li
-      Install GitLab Runner software.
-      Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+      Install a Runner compatible with GitLab CI
+      (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
     %li
-      Specify the following URL during runner setup:
+      Specify the following URL during the Runner setup:
       %code #{ci_root_url(only_path: false)}
     %li
       Use the following registration token during setup:
       %code #{@project.runners_token}
     %li
-      Start runner!
+      Start the Runner!
 
 
 - if @project_runners.any?
@@ -26,4 +26,4 @@
   %h4.underlined-title Available specific runners
   %ul.bordered-list.available-specific-runners
     = render partial: 'runner', collection: @assignable_runners, as: :runner
-  = paginate @assignable_runners
+  = paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 2d5b9f43c24da3103ec4c02b3ca9cba2253376d0..92957470070cccf51dc020d586ba0ae7ba612221 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -2,24 +2,24 @@
 
 .light.prepend-top-default
   %p
-    A 'runner' is a process which runs a build.
-    You can setup as many runners as you need.
+    A 'Runner' is a process which runs a build.
+    You can setup as many Runners as you need.
     %br
     Runners can be placed on separate users, servers, and even on your local machine.
 
-  %p Each runner can be in one of the following states:
+  %p Each Runner can be in one of the following states:
   %div
     %ul
       %li
         %span.label.label-success active
-        \- runner is active and can process any new build
+        \- Runner is active and can process any new builds
       %li
         %span.label.label-danger paused
-        \- runner is paused and will not receive any new build
+        \- Runner is paused and will not receive any new builds
 
 %hr
 
-%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
 .row
   .col-sm-6
     = render 'specific_runners'
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 752fbc21a111e9cfbb56ea5d637c58b03fef4b99..b41edeb2c7eaaa49edb1340d031ca2b54404df39 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -12,6 +12,9 @@
       = form.submit 'Save changes', class: 'btn btn-save'
       &nbsp;
       - if @service.valid? && @service.activated?
-        - disabled = @service.can_test? ? '':'disabled'
-        = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled}", title: @service.disabled_title
+        - unless @service.can_test?
+          - disabled_class = 'disabled'
+          - disabled_title = @service.disabled_title
+
+        = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
       = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index a666d07e9ebac91030631e50cc616623b4c95481..4de95036eeff7dabbe9cf2a437384618713044b4 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,71 +12,74 @@
 = render 'projects/last_push'
 = render "home_panel"
 
-%nav.project-stats{ class: (container_class) }
-  %ul.nav
-    %li
-      = link_to project_files_path(@project) do
-        Files (#{repository_size})
-    %li
-      = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
-        #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.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
+- if current_user && can?(current_user, :download_code, @project)
+  %nav.project-stats{ class: container_class }
+    %ul.nav
       %li
-        = link_to 'Readme', readme_path(@project)
-
-    - if @repository.changelog
+        = link_to project_files_path(@project) do
+          Files (#{repository_size})
       %li
-        = link_to 'Changelog', changelog_path(@project)
-
-    - if @repository.license_blob
+        = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+          #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
       %li
-        = link_to license_short_name(@project), license_path(@project)
-
-    - if @repository.contribution_guide
+        = link_to namespace_project_branches_path(@project.namespace, @project) do
+          #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
       %li
-        = link_to 'Contribution guide', contribution_guide_path(@project)
+        = link_to namespace_project_tags_path(@project.namespace, @project) do
+          #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
 
-    - 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
-    %li.project-repo-buttons-right
-      .project-repo-buttons.project-right-buttons
-        - if current_user
-          = render 'shared/members/access_request_buttons', source: @project
-
-        .btn-group.project-repo-btn-group
-          = render "projects/buttons/download"
-          = render 'projects/buttons/dropdown'
-
-        = render 'shared/notifications/button', notification_setting: @notification_setting
-- if @repository.commit
-  .project-last-commit{ class: container_class }
-    = render 'projects/last_commit', commit: @repository.commit, project: @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.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.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
+
+      %li.project-repo-buttons-right
+        .project-repo-buttons.project-right-buttons
+          - if current_user
+            = render 'shared/members/access_request_buttons', source: @project
+            = render "projects/buttons/koding"
+
+          .btn-group.project-repo-btn-group
+            = render 'projects/buttons/download', project: @project, ref: @ref
+            = render 'projects/buttons/dropdown'
+
+          = render 'shared/notifications/button', notification_setting: @notification_setting
+  - if @repository.commit
+    .project-last-commit{ class: container_class }
+      = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
 
 %div{ class: container_class }
   - if @project.archived?
@@ -85,5 +88,7 @@
         = icon("exclamation-triangle fw")
         Archived project! Repository is read-only
 
-  %div{class: "project-show-#{default_project_view}"}
-    = render default_project_view
\ No newline at end of file
+  - view_path = default_project_view
+
+  %div{ class: project_child_container_class(view_path) }
+    = render view_path
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index bdbf3e5f4d6b875aed2f0e016935408a538c5168..32e1f8a21b004a4de7ca7e35358593a179e57f8b 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,29 +1,29 @@
 .hidden-xs
   - if can?(current_user, :create_project_snippet, @project)
-    = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
-      New Snippet
+    = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
+      New snippet
+  - if can?(current_user, :update_project_snippet, @snippet)
+    = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
+      Delete
   - if can?(current_user, :update_project_snippet, @snippet)
     = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
       Edit
-  - if can?(current_user, :update_project_snippet, @snippet)
-    = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
-      Delete
 - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
   .visible-xs-block.dropdown
     %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
       Options
-      %span.caret
+      = icon('caret-down')
     .dropdown-menu.dropdown-menu-full-width
       %ul
         - if can?(current_user, :create_project_snippet, @project)
           %li
-            = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do
-              New Snippet
-        - if can?(current_user, :update_project_snippet, @snippet)
-          %li
-            = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
-              Edit
+            = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New snippet" do
+              New snippet
         - if can?(current_user, :update_project_snippet, @snippet)
           %li
             = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
               Delete
+        - if can?(current_user, :update_project_snippet, @snippet)
+          %li
+            = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
+              Edit
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 1646bcf4b8ab023bd9e6cf694a61caecace0bf10..e77e1b026f6ec236b76104d6108def4968c9fa3a 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,10 +1,9 @@
 - page_title "Snippets"
 
 .sub-header-block
-  .pull-right
-    - if can?(current_user, :create_project_snippet, @project)
-      = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
-        New Snippet
+  - if can?(current_user, :create_project_snippet, @project)
+    = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+      New snippet
 
   .oneline
     Share code pastes with others out of git repository
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index bae4d8f349f2c661db9a32d740a5672a60e94017..9503dbded13ae43766c19c3f64b52e5811832aef 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,15 +1,17 @@
 - page_title @snippet.title, "Snippets"
 
-.snippet-holder
-  = render 'shared/snippets/header'
+= render 'shared/snippets/header'
 
-  %article.file-holder.file-holder-no-border.snippet-file-content
-    .file-title.file-title-clear
+.project-snippets
+  %article.file-holder.snippet-file-content
+    .file-title
       = blob_icon 0, @snippet.file_name
       = @snippet.file_name
-      .file-actions.hidden-xs
+      .file-actions
         = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
         = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
     = render 'shared/snippets/blob'
 
+  = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
   %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
deleted file mode 100644
index 8a11dbfa9f4adfc4f4a9da1229ee8f31c5db155c..0000000000000000000000000000000000000000
--- a/app/views/projects/tags/_download.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%span.btn-group
-  = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
-    %span Source code
-  %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
-    %span.caret
-    %span.sr-only
-      Select Archive Format
-  %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
-    %li
-      = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
-        %span Download zip
-    %li
-      = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
-        %span Download tar.gz
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 2c11c0e5b21801fbccc291df013bf22252a98f0a..c42641afea0cbaffcdc6ee57a389a650967c6a91 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,4 +1,4 @@
-- commit = @repository.commit(tag.target)
+- commit = @repository.commit(tag.dereferenced_target)
 - release = @releases.find { |release| release.tag == tag.name }
 %li
   %div
@@ -11,8 +11,7 @@
       = strip_gpg_signature(tag.message)
 
     .controls
-      - if can?(current_user, :download_code, @project)
-        = render 'projects/tags/download', ref: tag.name, project: @project
+      = render 'projects/buttons/download', project: @project, ref: 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
@@ -31,4 +30,4 @@
     .description.prepend-top-default
       .wiki
         = preserve do
-          = markdown release.description
+          = markdown_field(release, :description)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 368231e73fe6cae1daa6c85b64badebad0b2aa99..7a0d9dcc94f5a59f989a83a54dd7d7c64896cd30 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -8,21 +8,24 @@
       Tags give the ability to mark specific points in history as being important
 
     .nav-controls
-      - if can? current_user, :push_code, @project
-        = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
-          New tag
+      = form_tag(filter_tags_path, method: :get) do
+        = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
       .dropdown.inline
         %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
-          %span.light= @sort.humanize
-          %b.caret
+          %span.light
+            = @sort.humanize
+          = icon('caret-down')
         %ul.dropdown-menu.dropdown-menu-align-right
           %li
-            = link_to namespace_project_tags_path(sort: nil) do
+            = link_to filter_tags_path(sort: nil) do
               Name
-            = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do
+            = link_to filter_tags_path(sort: sort_value_recently_updated) do
               = sort_title_recently_updated
-            = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do
+            = link_to filter_tags_path(sort: sort_value_oldest_updated) do
               = sort_title_oldest_updated
+      - if can?(current_user, :push_code, @project)
+        = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
+          New tag
 
   .tags
     - if @tags.any?
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 395d7af6cbb4d033ea539e5c2fabfc0fe35fce79..155af75575915ec28b008fdce1f14fbf8fc2ca73 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -12,8 +12,7 @@
         = icon('files-o')
       = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
         = icon('history')
-      - if can? current_user, :download_code, @project
-        = render 'projects/tags/download', ref: @tag.name, project: @project
+      = render 'projects/buttons/download', project: @project, ref: @tag.name
       - if can?(current_user, :admin_project, @project)
         .pull-right
           = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
@@ -34,6 +33,6 @@
       .description
         .wiki
           = preserve do
-            = markdown @release.description
+            = markdown_field(@release, :description)
     - else
       This tag has no release notes.
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index a3a4dba3fa438fa26138ef0d9b52e5b03cb8c50c..ee417b58cbf5656706351150bff0d1b3a9628571 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -4,6 +4,6 @@
     - file_name = blob_item.name
     = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
       %span.str-truncated= file_name
-  %td.tree_time_ago.cgray
-    = render 'projects/tree/spinner'
-  %td.hidden-xs.tree_commit
+  %td.hidden-xs.tree-commit
+  %td.tree-time-ago.cgray.text-right
+    = render 'projects/tree/spinner'
\ No newline at end of file
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index baaa2caa6de827bee5791c25210feb364e31533e..a1f4e3e8ed6f326a3ea87408986d44ffa23d1041 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,7 @@
 %article.file-holder.readme-holder
   .file-title
     = blob_icon readme.mode, readme.name
-    = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
+    = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
       %strong
         = readme.name
   .file-content.wiki
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 558e6146ae993f7c860061ce3223fa21f3dd9149..21e378b87355a60518d3f5aa96af10977f029eda 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -4,7 +4,6 @@
       %thead
         %tr
           %th Name
-          %th Last Update
           %th.hidden-xs
             .pull-left Last Commit
             .last-commit.hidden-sm.pull-left
@@ -14,9 +13,11 @@
               %small.light
                 = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
                 &ndash;
-                = truncate(@commit.title, length: 50)
-            = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
-
+                = time_ago_with_tooltip(@commit.committed_date)
+                = @commit.full_title
+            %small.commit-history-link-spacer &#124;
+            = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link'
+          %th.text-right Last Update
       - if @path.present?
         %tr.tree-item
           %td.tree-item-file-name
@@ -36,5 +37,5 @@
 :javascript
   // Load last commit log for each file in tree
   $('#tree-slider').waitForImages(function() {
-    ajaxGet("#{escape_javascript(@logs_path)}");
+    gl.utils.ajaxGet("#{escape_javascript(@logs_path)}");
   });
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 9577696fc0daf3d61fc41ee4b4d01e6fe004a300..1ccef6d52abb35e6273531ebd7202ff2e6263efd 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -4,6 +4,6 @@
     - path = flatten_tree(tree_item)
     = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
       %span.str-truncated= path
-  %td.tree_time_ago.cgray
-    = render 'projects/tree/spinner'
-  %td.hidden-xs.tree_commit
+  %td.hidden-xs.tree-commit
+  %td.tree-time-ago.text-right
+    = render 'projects/tree/spinner'
\ No newline at end of file
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index bf5360b4deee9195834eca9859419a845d3ad686..9864be3562a8b1e8ab686a79bd9e109cb0322ded 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -4,14 +4,13 @@
 = 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")
-= render 'projects/last_push'
 = render "projects/commits/head"
+= render 'projects/last_push'
 
 %div{ class: container_class }
   .tree-controls
     = render 'projects/find_file_link'
-    - if can? current_user, :download_code, @project
-      = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
+    = render 'projects/buttons/download', project: @project, ref: @ref
 
   #tree-holder.tree-holder.clearfix
     .nav-block
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index 7f3de47d7df4315554ebff11adee6a6bf4700cf5..f6e0b0a7c8a3b0246d469bedb400866780ba3af4 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -4,65 +4,89 @@
   .col-lg-3
     %h4.prepend-top-0
       = page_title
-    %p
-      Triggers can force a specific branch or tag to rebuild with an API call.
+    %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'
   .col-lg-9
-    %h5.prepend-top-0
-      Your triggers
-    - if @triggers.any?
-      .table-responsive
-        %table.table
-          %thead
-            %th Token
-            %th Last used
-            %th
-          = render partial: '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.
+    .panel.panel-default
+      .panel-heading
+        %h4.panel-title
+          Manage your project's triggers
+      .panel-body
+        - if @triggers.any?
+          .table-responsive
+            %table.table
+              %thead
+                %th
+                  %strong Token
+                %th
+                  %strong Last used
+                %th
+              = render partial: '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'
+        = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
+          = f.submit "Add trigger", class: 'btn btn-success'
 
-    %h5.prepend-top-default
-      Use CURL
+      .panel-footer
 
-    %p.light
-      Copy the token above, set your branch or tag name, and that reference will be rebuilt.
+        %p
+          In the following examples, you can see the exact API call you need to
+          make in order to rebuild a specific
+          %code ref
+          (branch or tag) with a trigger token.
+        %p
+          All you need to do is replace the
+          %code TOKEN
+          and
+          %code REF_NAME
+          with the trigger token and the branch or tag name respectively.
 
-    %pre
-      :plain
-        curl -X POST \
-             -F token=TOKEN \
-             -F ref=REF_NAME \
-             #{builds_trigger_url(@project.id)}
-    %h5.prepend-top-default
-      Use .gitlab-ci.yml
+        %h5.prepend-top-default
+          Use cURL
 
-    %p.light
-      In the
-      %code .gitlab-ci.yml
-      of the dependent project, include the following snippet.
-      The project will rebuild at the end of the build.
+        %p.light
+          Copy one of the tokens above, set your branch or tag name, and that
+          reference will be rebuilt.
 
-    %pre
-      :plain
-        trigger:
-          type: deploy
-          script:
-            - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
-    %h5.prepend-top-default
-      Pass build variables
+        %pre
+          :plain
+            curl -X POST \
+                 -F token=TOKEN \
+                 -F ref=REF_NAME \
+                 #{builds_trigger_url(@project.id)}
+        %h5.prepend-top-default
+          Use .gitlab-ci.yml
 
-    %p.light
-      Add
-      %code variables[VARIABLE]=VALUE
-      to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+        %p.light
+          In the
+          %code .gitlab-ci.yml
+          of another project, include the following snippet.
+          The project will be rebuilt at the end of the build.
 
-    %pre.append-bottom-0
-      :plain
-        curl -X POST \
-             -F token=TOKEN \
-             -F "ref=REF_NAME" \
-             -F "variables[RUN_NIGHTLY_BUILD]=true" \
-             #{builds_trigger_url(@project.id)}
+        %pre
+          :plain
+            trigger_build:
+              stage: deploy
+              script:
+                - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
+        %h5.prepend-top-default
+          Pass build variables
+
+        %p.light
+          Add
+          %code variables[VARIABLE]=VALUE
+          to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+
+        %pre.append-bottom-0
+          :plain
+            curl -X POST \
+                 -F token=TOKEN \
+                 -F "ref=REF_NAME" \
+                 -F "variables[RUN_NIGHTLY_BUILD]=true" \
+                 #{builds_trigger_url(@project.id)}
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 6c43f822db495716a631aa1103e4872d18195b8e..07cee86ba4ce0c7bbc1eb10b328495ef476d3d91 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -9,7 +9,7 @@
       %th Value
       %th
     %tbody
-      - @project.variables.each do |variable|
+      - @project.variables.order_key_asc.each do |variable|
         - if variable.id?
           %tr
             %td= variable.key
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 643f7c589e6545243e5448eb967d63daf2a388eb..4e41a15d9f4e8cd96d2673e0766ce03454ae9713 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -24,7 +24,7 @@
 
         = succeed '.' do
           More examples are in the
-          = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown")
+          = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown")
 
   .form-group
     = f.label :commit_message, class: 'control-label'
@@ -33,7 +33,12 @@
   .form-actions
     - if @page && @page.persisted?
       = f.submit 'Save changes', class: "btn-save btn"
-      = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel"
+      .pull-right
+        - if can?(current_user, :admin_wiki, @project)
+          = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
+            Delete
+        = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
     - else
       = f.submit 'Create page', class: "btn-create btn"
-      = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel"
+      .pull-right
+        = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel"
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 4ea75dbbf0cdf4d0b738b7cdf62ac8d6b64214f4..763c2fea39b4eea90d5f55e65783c395a073c3db 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -7,6 +7,3 @@
   - if can?(current_user, :create_wiki, @project)
     = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
       Edit
-  - if can?(current_user, :admin_wiki, @project)
-    = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
-      Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index f8ea479e0b113e90f0285b678037b891e7f97602..09c4411d67e35e63ed3191761d7acb2870715264 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,16 @@
-.nav-links.sub-nav
-  %ul{ class: (container_class) }
-    = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
-      = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+= 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(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+          = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
 
-    = nav_link(path: 'wikis#pages') do
-      = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+        = nav_link(path: 'wikis#pages') do
+          = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
 
-    = nav_link(path: 'wikis#git_access') do
-      = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
-        Git Access
+        = nav_link(path: 'wikis#git_access') do
+          = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+            Git Access
 
-  = render 'projects/wikis/new'
+      = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 233538bb488cff19f8c1606c02ea9cb36c8dacfe..679d6018befedc3f46a0aacd24d5ee40d7cfe8a0 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -19,7 +19,5 @@
         - if can?(current_user, :create_wiki, @project)
           = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
             New Page
-      = render 'main_links'
-
 
   = render 'form'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 252c37532e16370d1f4cd06145947ff2627c5772..7fe2bce3e7c35cbff0bfa6d5a62545479f99b759 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -10,12 +10,16 @@
         in group #{link_to @group.name, @group}
 
   .results.prepend-top-10
-    .search-results
-      - if @scope == 'projects'
-        .term
-          = render 'shared/projects/list', projects: @search_objects
-      - else
-        = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+    - if @scope == 'commits'
+      %ul.list-unstyled
+        = render partial: "search/results/commit", collection: @search_objects
+    - else
+      .search-results
+        - if @scope == 'projects'
+          .term
+            = render 'shared/projects/list', projects: @search_objects
+        - else
+          = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
 
     - if @scope != 'projects'
       = paginate(@search_objects, theme: 'gitlab')
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 290743feb4a57c4fb80a343e67188fad2a1b9d27..6f0a0ea36ec69cf1f469f6ef5646548169549b66 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,4 +1,4 @@
-- blob = @project.repository.parse_search_result(blob)
+- blob = parse_search_result(blob)
 .blob-result
   .file-holder
     .file-title
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 4e6c3965dc65dac0a96f427c8466b2091df7b37e..f34eaf89027965c1f89b84bc4462b5ba369f3107 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1,2 +1 @@
-.search-result-row
-  = render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 8f68d6d1b87048d28cbbccc6cb743b9943ef4c64..e010f21de5a0255e9c1e63924603b9cf40a940ef 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -7,7 +7,7 @@
   - if issue.description.present?
     .description.term
       = preserve do
-        = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
+        = search_md_sanitize(issue, :description)
   %span.light
     #{issue.project.name_with_namespace}
   - if issue.closed?
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 6331c2bd6b044e146756b163cc6e6eda64a8d3b1..07b17bc69c06953195d552dbb1e1d327641e7658 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
   - if merge_request.description.present?
     .description.term
       = preserve do
-        = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
+        = search_md_sanitize(merge_request, :description)
   %span.light
     #{merge_request.project.name_with_namespace}
   .pull-right
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index b31595d8d1c5a4fe9dc4c2a9ce115ff559650287..9664f65a36e016140be5de9f152991af07d3fa4f 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
   - if milestone.description.present?
     .description.term
       = preserve do
-        = search_md_sanitize(markdown(milestone.description))
+        = search_md_sanitize(milestone, :description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index e040008387034a27bca2109e4d76ee0daa9f0877..f3701b89bb4709f76f3922800ddc56ae52aa97c9 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -23,4 +23,4 @@
   .note-search-result
     .term
       = preserve do
-        = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
+        = search_md_sanitize(note, :note)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 235106c4f746553abf0ecf928cbf989516d9d82d..648d0bd76cbf42558392dfd81cb6f436e8b78f57 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,4 +1,4 @@
-- wiki_blob = @project.repository.parse_search_result(wiki_blob)
+- wiki_blob = parse_search_result(wiki_blob)
 .blob-result
   .file-holder
     .file-title
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..9ce6a1aeef552ebf4b10a9c6bfd85c36ad1c3fff
--- /dev/null
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -0,0 +1,19 @@
+- noteable = @sent_notification.noteable
+- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
+- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+
+- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
+
+
+%h3.page-title
+  Unsubscribe from #{noteable_type} #{noteable_text}
+
+%p
+  = succeed '?' do
+    Are you sure you want to unsubscribe from #{noteable_type}
+    = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
+
+%p
+  = link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true),
+            class: 'btn btn-primary append-right-10'
+  = link_to 'Cancel', new_user_session_path, class: 'btn append-right-10'
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8824bcc158e6455ffe80506181aebcfe558b094f..c367ae336db288021750db2f10328cd50887b845 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,5 +1,9 @@
 %ul.nav-links.event-filter.scrolling-tabs
-  = event_filter_link EventFilter.push, 'Push events'
-  = event_filter_link EventFilter.merged, 'Merge events'
-  = event_filter_link EventFilter.comments, 'Comments'
+  = event_filter_link EventFilter.all, 'All'
+  - if event_filter_visible(:repository)
+    = event_filter_link EventFilter.push, 'Push events'
+  - if event_filter_visible(:merge_requests)
+    = event_filter_link EventFilter.merged, 'Merge events'
+  - if event_filter_visible(:issues)
+    = event_filter_link EventFilter.comments, 'Comments'
   = event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 67072b9fc2abb513da29f06315bab8b921ec4edd..ba25e09d638abd54bf1cc2ddced24148d9ad852d 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -9,11 +9,13 @@
   = f.label :path, class: 'control-label' do
     Group path
   .col-sm-10
-    .input-group
+    .input-group.gl-field-error-anchor
       .input-group-addon
         = root_url
       = f.text_field :path, placeholder: 'open-source', class: 'form-control',
-          autofocus: local_assigns[:autofocus] || false
+        autofocus: local_assigns[:autofocus] || false,  pattern: "[a-zA-Z0-9-_]+",
+        required: true, title: 'Please choose a group name with no special characters.'
+
     - if @group.persisted?
       .alert.alert-warning.prepend-top-10
         %ul
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6ccdef0df465aa3cf0d2bc07b2b06f19c01c91bc
--- /dev/null
+++ b/app/views/shared/_label.html.haml
@@ -0,0 +1,54 @@
+- label_css_id = dom_id(label)
+- open_issues_count = label.open_issues_count(current_user)
+- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- subject = local_assigns[:subject]
+
+%li{id: label_css_id, data: { id: label.id } }
+  = render "shared/label_row", label: label
+
+  .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
+    %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
+      Options
+      = icon('caret-down')
+    .dropdown-menu.dropdown-menu-align-right
+      %ul
+        %li
+          = link_to_label(label, subject: subject, type: :merge_request) do
+            = pluralize open_merge_requests_count, 'merge request'
+        %li
+          = link_to_label(label, subject: subject) do
+            = pluralize open_issues_count, 'open issue'
+        - if current_user
+          %li.label-subscription{ data: toggle_subscription_data(label) }
+            %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
+              %span= label_subscription_toggle_button_text(label)
+        - if can?(current_user, :admin_label, label)
+          %li
+            = link_to 'Edit', edit_label_path(label)
+          %li
+            = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
+
+  .pull-right.hidden-xs.hidden-sm.hidden-md
+    = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+      = pluralize open_merge_requests_count, 'merge request'
+    = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
+      = pluralize open_issues_count, 'open issue'
+
+    - if current_user
+      .label-subscription.inline{ data: toggle_subscription_data(label) }
+        %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
+          %span.sr-only= label_subscription_toggle_button_text(label)
+          = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
+          = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+    - if can?(current_user, :admin_label, label)
+      = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
+        %span.sr-only Edit
+        = icon('pencil-square-o')
+      = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
+        %span.sr-only Delete
+        = icon('trash-o')
+
+  - if current_user && label.is_a?(ProjectLabel)
+    :javascript
+      new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 77676454b57f863fa70e97b6543866564cc08301..d28f9421ecf6c2bfc77271ef8af21b8eafd06404 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -3,13 +3,16 @@
     .draggable-handler
       = icon('bars')
     .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
-      dom_id: dom_id(label) } }
+      dom_id: dom_id(label), type: label.type } }
       %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
         = icon('star-o')
       %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
         = icon('star')
   %span.label-name
-    = link_to_label(label, tooltip: false)
+    = link_to_label(label, subject: @project, tooltip: false)
+  - if defined?(@project) && @project.group.present?
+    %span.label-type
+      = label.model_name.human.titleize
   - if label.description
     %span.label-description
-      = markdown(label.description, pipeline: :single_line)
+      = markdown_field(label, :description)
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index e324d0e5203e4a1dbe51d6438b97fc46f9cad6e3..21b37a7c9ae0a0fbce6c2dfccd1375132c65e5b3 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,5 +1,5 @@
 - labels.each do |label|
   %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
-    = link_to_label(label, css_class: 'btn btn-transparent')
+    = link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
     %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
       = icon("times")
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index b07f1c5603e6cbf7b3fe53f6e64055140f11ddbc..9b67422da2c2532586fc689dfabee1acaef34fc1 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,9 +1,9 @@
-<svg width="36" height="36" id="tanuki-logo">
-  <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
-  <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
-  <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
-  <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
-  <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
-  <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
-  <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
+<svg width="36" height="36" class="tanuki-logo">
+  <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"/>
+  <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+  <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+  <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+  <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
 </svg>
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cf16c203f9c89eee95c186fbcaf88d41c196f5e1..73d288e22366f32229b4761c5e2032691b4d81d1 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,10 +1,19 @@
+- if @project
+  - counts = milestone_counts(@project.milestones)
+
 %ul.nav-links
-  %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
+  %li{class: milestone_class_for_state(params[:state], 'opened', true)}
     = link_to milestones_filter_path(state: 'opened') do
       Open
-  %li{class: ("active" if params[:state] == 'closed')}
+      - if @project
+        %span.badge #{counts[:opened]}
+  %li{class: milestone_class_for_state(params[:state], 'closed')}
     = link_to milestones_filter_path(state: 'closed') do
       Closed
-  %li{class: ("active" if params[:state] == 'all')}
+      - if @project
+        %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]}
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4e3b1b3a571da49e13ca1edc61a845980bcfcbb2
--- /dev/null
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -0,0 +1,4 @@
+.fade-left
+  = icon('angle-left')
+.fade-right
+  = icon('angle-right')
\ No newline at end of file
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 51622931e24c0cf422cc47c4b355e690fcaea11b..fbbf6f358c5e5a7df1d61b0aaa8c85c466ba6a3f 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -3,7 +3,7 @@
     = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
     %a.btn.btn-new.new-project-item-select-button
       = local_assigns[:label]
-      %b.caret
+      = icon('caret-down')
 
   :javascript
     $('.new-project-item-select-button').on('click', function() {
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index ea7162d4d63a54f1818ade14e298ff3fc9e85857..9a8252ab0871b03ace13067dbcceb8206dcf130a 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,7 +6,7 @@
   - @options && @options.each do |key, value|
     = hidden_field_tag key, value, id: nil
   .dropdown
-    = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+    = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
     .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
       = dropdown_title "Switch branch/tag"
       = dropdown_filter "Search branches and tags"
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 249bce926ceec281da29c82e84af5f4140bf2526..68e05cb72e17ecfc69d106bc55d0b07c8a829388 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -5,29 +5,29 @@
       = sort_options_hash[@sort]
     - else
       = sort_title_recently_created
-    %b.caret
+    = icon('caret-down')
   %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
     %li
-      = link_to page_filter_path(sort: sort_value_priority) do
+      = link_to page_filter_path(sort: sort_value_priority, label: true) do
         = sort_title_priority
-      = link_to page_filter_path(sort: sort_value_recently_created) do
+      = 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) do
+      = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
         = sort_title_oldest_created
-      = link_to page_filter_path(sort: sort_value_recently_updated) do
+      = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do
         = sort_title_recently_updated
-      = link_to page_filter_path(sort: sort_value_oldest_updated) do
+      = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
         = sort_title_oldest_updated
-      = link_to page_filter_path(sort: sort_value_milestone_soon) do
+      = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
         = sort_title_milestone_soon
-      = link_to page_filter_path(sort: sort_value_milestone_later) do
+      = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
         = sort_title_milestone_later
       - if controller.controller_name == 'issues' || controller.action_name == 'issues'
-        = link_to page_filter_path(sort: sort_value_due_date_soon) do
+        = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
           = sort_title_due_date_soon
-        = link_to page_filter_path(sort: sort_value_due_date_later) do
+        = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
           = sort_title_due_date_later
-      = link_to page_filter_path(sort: sort_value_upvotes) do
+      = link_to page_filter_path(sort: sort_value_upvotes, label: true) do
         = sort_title_upvotes
-      = link_to page_filter_path(sort: sort_value_downvotes) do
+      = link_to page_filter_path(sort: sort_value_downvotes, label: true) do
         = sort_title_downvotes
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 107ad19177c9e1a314923ef3109db4dc12cf403a..b11257ee0e664932352d14bee1d6a4e500d2a303 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,12 +1,12 @@
 .form-group.project-visibility-level-holder
   = f.label :visibility_level, class: 'control-label' do
     Visibility Level
-    = link_to "(?)", help_page_path("public_access/public_access")
+    = link_to icon('question-circle'), help_page_path("public_access/public_access")
   .col-sm-10
     - if can_change_visibility_level
       = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
     - else
-      .col-sm-10
+      %div
         %span.info
           = visibility_level_icon(visibility_level)
           %strong
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index ebe2eb0433d4a45ad4c05f6636c6f95cf441201c..182c4eebd503a0358da5e5204ddb9b9c16ffc86b 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -10,6 +10,6 @@
       .option-descr
         = visibility_level_description(level, form_model)
 - unless restricted_visibility_levels.empty?
-  .col-sm-10
+  %div
     %span.info
       Some visibility level settings have been restricted by the administrator.
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..60353aee7f15750eaf4611a954771dc9fda6a6dd
--- /dev/null
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -0,0 +1,24 @@
+%ul.nav-links
+  %li{ class: ('active' if scope.nil?) }
+    = link_to build_path_proc.call(nil) do
+      All
+      %span.badge.js-totalbuilds-count
+        = number_with_delimiter(all_builds.count(:id))
+
+  %li{ class: ('active' if scope == 'pending') }
+    = link_to build_path_proc.call('pending') do
+      Pending
+      %span.badge
+        = number_with_delimiter(all_builds.pending.count(:id))
+
+  %li{ class: ('active' if scope == 'running') }
+    = link_to build_path_proc.call('running') do
+      Running
+      %span.badge
+        = number_with_delimiter(all_builds.running.count(:id))
+
+  %li{ class: ('active' if scope == 'finished') }
+    = link_to build_path_proc.call('finished') do
+      Finished
+      %span.badge
+        = number_with_delimiter(all_builds.finished.count(:id))
diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/_todos_all_done.svg
new file mode 100644
index 0000000000000000000000000000000000000000..94b5c2e0ea0928b6cb0c171b0147e1deb6ac1690
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_all_done.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/_todos_empty.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b1e661268fb12f8db313f9f44670dca94567daf0
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_empty.svg
@@ -0,0 +1,110 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs>
+    <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/>
+    <mask id="l" width="180" height="220" x="0" y="0" fill="#fff">
+      <use xlink:href="#a"/>
+    </mask>
+    <rect id="b" width="180" height="220" rx="6"/>
+    <mask id="m" width="180" height="220" x="0" y="0" fill="#fff">
+      <use xlink:href="#b"/>
+    </mask>
+    <rect id="c" width="28" height="28" rx="4"/>
+    <mask id="n" width="28" height="28" x="0" y="0" fill="#fff">
+      <use xlink:href="#c"/>
+    </mask>
+    <rect id="d" width="28" height="28" rx="4"/>
+    <mask id="o" width="28" height="28" x="0" y="0" fill="#fff">
+      <use xlink:href="#d"/>
+    </mask>
+    <circle id="e" cx="21.5" cy="21.5" r="21.5"/>
+    <mask id="p" width="43" height="43" x="0" y="0" fill="#fff">
+      <use xlink:href="#e"/>
+    </mask>
+    <circle id="f" cx="26.5" cy="26.5" r="26.5"/>
+    <mask id="q" width="53" height="53" x="0" y="0" fill="#fff">
+      <use xlink:href="#f"/>
+    </mask>
+    <circle id="g" cx="9.5" cy="4.5" r="4.5"/>
+    <mask id="r" width="13" height="13" x="-2" y="-2">
+      <path fill="#fff" d="M3-2h13v13H3z"/>
+      <use xlink:href="#g"/>
+    </mask>
+    <circle id="h" cx="26.5" cy="26.5" r="26.5"/>
+    <mask id="s" width="53" height="53" x="0" y="0" fill="#fff">
+      <use xlink:href="#h"/>
+    </mask>
+    <circle id="i" cx="21.5" cy="21.5" r="21.5"/>
+    <mask id="t" width="43" height="43" x="0" y="0" fill="#fff">
+      <use xlink:href="#i"/>
+    </mask>
+    <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/>
+    <mask id="u" width="52" height="44" x="0" y="0" fill="#fff">
+      <use xlink:href="#j"/>
+    </mask>
+    <circle id="k" cx="18.5" cy="18.5" r="18.5"/>
+    <mask id="v" width="37" height="37" x="0" y="0" fill="#fff">
+      <use xlink:href="#k"/>
+    </mask>
+  </defs>
+  <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)">
+    <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/>
+    <g transform="rotate(5 -707.333 618.042)">
+      <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/>
+      <g transform="translate(29 24)">
+        <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+        <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+        <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+        <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+      </g>
+      <g transform="translate(29 69)">
+        <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+        <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+        <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/>
+        <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/>
+      </g>
+      <g transform="translate(28 160)">
+        <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/>
+        <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/>
+        <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/>
+      </g>
+      <g transform="translate(28 116)">
+        <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/>
+        <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/>
+        <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/>
+      </g>
+    </g>
+    <g transform="rotate(-15 601.917 -782.362)">
+      <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/>
+      <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1">
+        <tspan x="12" y="27">@</tspan>
+      </text>
+    </g>
+    <g transform="rotate(15 -686.59 1035.907)">
+      <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/>
+      <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/>
+      <g transform="translate(17 14)">
+        <use fill="#FC6D26" xlink:href="#g"/>
+        <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/>
+      </g>
+    </g>
+    <g transform="rotate(15 -85.125 65.185)">
+      <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/>
+      <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/>
+    </g>
+    <g transform="rotate(-15 716.492 78.873)">
+      <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/>
+      <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/>
+    </g>
+    <g transform="rotate(-15 129.114 -585.74)">
+      <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/>
+      <circle cx="16" cy="20" r="2" fill="#FC6D26"/>
+      <circle cx="27" cy="20" r="2" fill="#FC6D26"/>
+      <circle cx="38" cy="20" r="2" fill="#FC6D26"/>
+    </g>
+    <g transform="rotate(-15 1254.8 -458.986)">
+      <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/>
+      <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/>
+      <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/>
+    </g>
+  </g>
+</svg>
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 1ad953510056feb56e98f72dd76124d5d63aa94a..19221e3391fcb16bf3f70ce06d85c8acc70a43b8 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -24,7 +24,8 @@
     %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
       = visibility_level_icon(group.visibility_level, fw: false)
 
-  = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+  .avatar-container.s40
+    = image_tag group_icon(group), class: "avatar s40 hidden-xs"
   .title
     = link_to group, class: 'group-name' do
       = group.name
@@ -35,4 +36,4 @@
 
   - if group.description.present?
     .description
-      = markdown(group.description, pipeline: :description)
+      = markdown_field(group, :description)
diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9d62012518be92615105796f7ef3810af8b144b4
--- /dev/null
+++ b/app/views/shared/icons/_icon_close.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
\ No newline at end of file
diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
new file mode 100644
index 0000000000000000000000000000000000000000..eb5a962d651a452b7d5da4db2b83bcb923e3cb68
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg>
diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9228be05f03303b960b82c204e1fee4b0aab5e5c
--- /dev/null
+++ b/app/views/shared/icons/_icon_empty_groups.svg
@@ -0,0 +1 @@
+<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
\ No newline at end of file
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index a21f8f3a95151f554d21afc9d66d0f7df16ce570..ce22b6cdaea7e9205f5ab3e47082d4d447c7fbe3 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
-  <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"><path fill="#7E7E7E" fill-rule="evenodd" d="M22 29.535V22.72c4.19-1.21 7.4-5.917 7.4-5.917.34-.446.6-1.247.6-1.795v-4.543C31.196 9.773 32 8.48 32 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 1.48.804 2.773 2 3.465v4.243c0 .17-.094.39-.21.502 0 0-1.253 1.203-1.84 1.742C22.5 18.277 21.12 19 20.09 19c-1.042 0-2.473-.74-3.977-2.086-.61-.546-1.746-1.574-1.746-1.574-.2-.182-.367-.555-.367-.83v-4.045C15.196 9.773 16 8.48 16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 1.48.804 2.773 2 3.465v4.543c0 .537.274 1.345.61 1.78 0 0 3.25 4.59 7.39 5.88v6.867c-1.196.692-2 1.984-2 3.465 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.48-.804-2.773-2-3.465zM14 7c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm16 0c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm-8 26c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2z"/></svg>
diff --git a/app/views/shared/icons/_icon_no_wrap.svg b/app/views/shared/icons/_icon_no_wrap.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fe34cada0023b37b17f1837c92515d3224af835b
--- /dev/null
+++ b/app/views/shared/icons/_icon_no_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <path fill-rule="evenodd" d="m6 11h-4.509c-.263 0-.491.226-.491.505v.991c0 .291.22.505.491.505h4.509v.679c0 .301.194.413.454.236l2.355-1.607c.251-.171.259-.442 0-.619l-2.355-1.607c-.251-.171-.454-.07-.454.236v.681m-5-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m10 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991m-10-4c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e965afa9a56100d1f044c8c3b429215f3b0c111d
--- /dev/null
+++ b/app/views/shared/icons/_icon_play.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play">
+  <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/>
+  </svg>
\ No newline at end of file
diff --git a/app/views/shared/icons/_icon_soft_wrap.svg b/app/views/shared/icons/_icon_soft_wrap.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ea27a2024b132752809a606d1fb20dccf7ac88c1
--- /dev/null
+++ b/app/views/shared/icons/_icon_soft_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+  <path fill-rule="evenodd" d="m12 11h-2v-.681c0-.307-.203-.407-.454-.236l-2.355 1.607c-.259.177-.251.448 0 .619l2.355 1.607c.259.177.454.065.454-.236v-.679h2c0 0 0 0 0 0 1.657 0 3-1.343 3-3 0-.828-.336-1.578-.879-2.121-.543-.543-1.293-.879-2.121-.879-.001 0-.002 0-.002 0h-10.497c-.271 0-.5.226-.5.505v.991c0 .291.224.505.5.505h10.497c.001 0 .002 0 .002 0 .552 0 1 .448 1 1 0 .276-.112.526-.293.707-.181.181-.431.293-.707.293m-11-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m0 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_status_cancel.svg b/app/views/shared/icons/_icon_status_canceled.svg
similarity index 79%
rename from app/views/shared/icons/_icon_status_cancel.svg
rename to app/views/shared/icons/_icon_status_canceled.svg
index fd1ebbcbabda8928110260ab8877f317f6a70c07..1b2d0891244abf60dc1859e44b19a952ffd54806 100644
--- a/app/views/shared/icons/_icon_status_cancel.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
@@ -1,4 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-canceled" viewBox="0 0 14 14">
   <g fill="#5C5C5C" fill-rule="evenodd">
     <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
     <rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/>
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dca5d2897674bb947bd247aca80c1af9e001a646
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-created" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
new file mode 100644
index 0000000000000000000000000000000000000000..014ca86b61b014e9e7a11d9db4492dd9bb215617
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -0,0 +1 @@
+<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4f9d9add60dec05f52ee2bf6be1629848883c885
--- /dev/null
+++ b/app/views/shared/icons/_illustration_no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
\ No newline at end of file
diff --git a/app/views/shared/icons/_next_discussion.svg b/app/views/shared/icons/_next_discussion.svg
new file mode 100644
index 0000000000000000000000000000000000000000..43559a60cb0f7ca43c92321edcb518e3fb141cdd
--- /dev/null
+++ b/app/views/shared/icons/_next_discussion.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c957cd84479f25a43fb5efe1b5f715b4af4c8002..ed93857e6d4bf7112b9411cf45e30566edb10915 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,9 +1,12 @@
+- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
+- boards_page = controller.controller_name == 'boards'
+
 .issues-filters
   .issues-details-filters.row-content-block.second-block
-    = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
-      - if params[:issue_search].present?
-        = hidden_field_tag :issue_search, params[:issue_search]
-      - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+    = 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]
+      - if @bulk_edit
         .check-all-holder
           = check_box_tag "check_all_issues", nil, false,
             class: "check_all_issues left"
@@ -12,26 +15,43 @@
           - if params[:author_id].present?
             = hidden_field_tag(:author_id, params[:author_id])
           = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
-            placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+            placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
 
         .filter-item.inline
           - if params[:assignee_id].present?
             = hidden_field_tag(:assignee_id, params[:assignee_id])
           = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
-            placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+            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"
+          = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true
 
         .filter-item.inline.labels-filter
-          = render "shared/issuable/label_dropdown"
+          = 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[]" }
+
+        - if issuable_filters_present
+          .filter-item.inline.reset-filters
+            %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
 
         .pull-right
-          = render 'shared/sort_dropdown'
+          - if boards_page
+            #js-boards-seach.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)
+                .dropdown.pull-right
+                  %button.btn.btn-create.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) } }
+                    Create new 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: "Create a new list" }
+                    - if can?(current_user, :admin_label, @project)
+                      = render partial: "shared/issuable/label_page_create"
+                    = dropdown_loading
+          - else
+            = render 'shared/sort_dropdown'
 
-    - if controller.controller_name == 'issues'
+    - if @bulk_edit
       .issues_bulk_update.hide
-        = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update'  do
+        = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update'  do
           .filter-item.inline
             = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
               %ul
@@ -54,15 +74,14 @@
                 %li
                   %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
 
-          = hidden_field_tag 'update[issues_ids]', []
+          = hidden_field_tag 'update[issuable_ids]', []
           = hidden_field_tag :state_event, params[:state_event]
           .filter-item.inline
-            = button_tag "Update issues", class: "btn update_selected_issues btn-save"
-
-  - if !@labels.nil?
-    .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
-      - if @labels.any?
-        = render "shared/labels_row", labels: @labels
+            = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
+  - has_labels = @labels && @labels.any?
+  .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
+    - if has_labels
+      = render 'shared/labels_row', labels: @labels
 
 :javascript
   new UsersSelect();
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 210b43c7e0b5a92f2afc36b9bc7fecf8ff172c64..3176af9c19b31c1e018eea68a45cca0c7c25f2cd 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,23 +1,20 @@
+- project = @target_project || @project
+
 = form_errors(issuable)
 
+- if @conflict
+  .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"
+    and make sure your changes will not unintentionally remove theirs
+
 .form-group
   = f.label :title, class: 'control-label'
 
-  - issuable_template_names = issuable_templates(issuable)
-
-  - if issuable_template_names.any?
-    .col-sm-3.col-lg-2
-      .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
-        - title = selected_template(issuable) || "Choose a template"
-
-        = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
-          title: title, filter: true, placeholder: 'Filter', footer_content: true,
-          data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
-          %ul.dropdown-footer-list
-            %li
-              %a.reset-template
-                Reset template
-  %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
+  = render 'shared/issuable/form/template_selector', issuable: issuable
+
+  %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
     = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
         class: 'form-control pad', required: true
 
@@ -42,7 +39,7 @@
     - if can_add_template?(issuable)
       %p.help-block
         Add
-        = link_to "issuable templates", help_page_path('workflow/description_templates')
+        = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
         to help your contributors communicate effectively!
 
 .form-group.detail-page-description
@@ -52,8 +49,9 @@
     = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
       = render 'projects/zen', f: f, attr: :description,
                                classes: 'note-textarea',
-                               placeholder: "Write a comment or drag your files here..."
-      = render 'projects/notes/hints'
+                               placeholder: "Write a comment or drag your files here...",
+                               supports_slash_commands: !issuable.persisted?
+      = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
       .clearfix
       .error-alert
 
@@ -74,38 +72,22 @@
         = f.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
-            = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
-                placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
-                selected: issuable.assignee_id, project: @target_project || @project,
-                first_user: true, current_user: true, include_blank: true)
-          %div
-            = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline'
+            - if issuable.assignee_id
+              = f.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: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
       .form-group.issue-milestone
         = f.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) }
-          - if milestone_options(issuable).present?
-            .issuable-form-select-holder
-              = f.select(:milestone_id, milestone_options(issuable),
-                { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
-          - else
-            .prepend-top-10
-            %span.light No open milestones available.
-          - if can? current_user, :admin_milestone, issuable.project
-            %div
-              = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+          .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"
       .form-group
-        - has_labels = issuable.project.labels.any?
+        - has_labels = @labels && @labels.any?
         = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+        = f.hidden_field :label_ids, multiple: true, value: ''
         .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
-          - if has_labels
-            .issuable-form-select-holder
-              = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
-                { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
-          - else
-            %span.light No labels yet.
-          - if can? current_user, :admin_label, issuable.project
-            %div
-              = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+          .issuable-form-select-holder
+            = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
     - if has_due_date
       .col-lg-6
         .form-group
@@ -120,13 +102,13 @@
     = label_tag :move_to_project_id, 'Move', class: 'control-label'
     .col-sm-10
       .issuable-form-select-holder
-        = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
+        = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
       &nbsp;
       %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
       title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
         = icon('question-circle')
 
-- if issuable.is_a?(MergeRequest)
+- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
   %hr
   - if @merge_request.new_record?
     .form-group
@@ -147,6 +129,7 @@
       .col-sm-10.col-sm-offset-2
         .checkbox
           = label_tag 'merge_request[force_remove_source_branch]' do
+            = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
             = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
             Remove source branch when merge request is accepted.
 
@@ -167,7 +150,9 @@
     = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
   - else
     .pull-right
-      - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+      - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
         = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} 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'
+
+= f.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d34d28f6736a11b63d98c13319e34f115d26f099..1d778bc88dec457a043cf6c53e190707f1ee2bd6 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,25 +1,30 @@
+- project = @target_project || @project
 - show_create = local_assigns.fetch(:show_create, true)
 - extra_options = local_assigns.fetch(:extra_options, true)
 - filter_submit = local_assigns.fetch(:filter_submit, true)
 - show_footer = local_assigns.fetch(:show_footer, true)
+- use_id = local_assigns.fetch(:use_id, true)
 - data_options = local_assigns.fetch(:data_options, {})
 - classes = local_assigns.fetch(:classes, [])
-- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
+- selected = local_assigns.fetch(:selected, nil)
+- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
+- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
 - dropdown_data.merge!(data_options)
 - classes << 'js-extra-options' if extra_options
 - classes << 'js-filter-submit' if filter_submit
 
-- if params[:label_name].present?
-  - if params[:label_name].respond_to?('any?')
-    - params[:label_name].each do |label|
-      = hidden_field_tag "label_name[]", label, id: nil
+- if selected
+  - selected.each do |label|
+    = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
+
 .dropdown
   %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
-    %span.dropdown-toggle-text
-      = h(multi_label_name(params[:label_name], "Label"))
-    = icon('chevron-down')
+    %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
+      = multi_label_name(selected, "Labels")
+    = icon('caret-down')
   .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
-    = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
-    - if show_create and @project and can?(current_user, :admin_label, @project)
+    = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
+    - if show_create && project && can?(current_user, :admin_label, project)
       = render partial: "shared/issuable/label_page_create"
     = dropdown_loading
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 4e280c371acc253702982ad989cf8860e59b2377..c0dc63be2bfb1a73cc92b4515beee2ae1cb35468 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -2,8 +2,16 @@
 - show_create = local_assigns.fetch(:show_create, true)
 - show_footer = local_assigns.fetch(:show_footer, true)
 - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
+- show_boards_content = local_assigns.fetch(:show_boards_content, false)
 .dropdown-page-one
   = dropdown_title(title)
+  - if show_boards_content
+    .issue-board-dropdown-content
+      %p
+        Each label that exists in your issue tracker can have its own dedicated
+        list. Select a label below to add a list to your Board and it will
+        automatically be populated with issues that have that label. To create
+        a list for a label that doesn't exist yet, simply create the label below.
   = dropdown_filter(filter_placeholder)
   = dropdown_content
   - if @project && show_footer
@@ -12,7 +20,7 @@
         - if can?(current_user, :admin_label, @project)
           %li
             %a.dropdown-toggle-page{href: "#"}
-              Create new
+              Create new label
         %li
           = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
             - if show_create && @project && can?(current_user, :admin_label, @project)
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 2fcf40ece99a063d7c7b1edc6487302b18bab2a2..40fe53e6a8daca6facd60ed80bd670ebd5d9ed60 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,16 +1,21 @@
-- if params[:milestone_title].present?
-  = hidden_field_tag(:milestone_title, params[:milestone_title])
-= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
-  placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
-  - if @project
+- project = @target_project || @project
+- extra_class = extra_class || ''
+- show_menu_above = show_menu_above || false
+- selected_text = selected.try(:title) || params[:milestone_title]
+- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
+- if selected.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
+  - if project
     %ul.dropdown-footer-list
-      - if can? current_user, :admin_milestone, @project
+      - if can? current_user, :admin_milestone, project
         %li
-          = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+          = link_to new_namespace_project_milestone_path(project.namespace, project), title: "New Milestone" do
             Create new
       %li
-        = link_to namespace_project_milestones_path(@project.namespace, @project) do
-          - if can? current_user, :admin_milestone, @project
+        = link_to namespace_project_milestones_path(project.namespace, project) do
+          - if can? current_user, :admin_milestone, project
             Manage milestones
           - else
             View milestones
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 1d9b09a5ef1429176bc912d53b24b7579337c644..5527a2f889a4699744a66e7b187729e5181e5b4e 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,25 +1,25 @@
+- type = local_assigns.fetch(:type, :issues)
+- page_context_word = type.to_s.humanize(capitalize: false)
+- issuables = @issues || @merge_requests
+
 %ul.nav-links.issues-state-filters
-  - if defined?(type) && type == :merge_requests
-    - page_context_word = 'merge requests'
-  - else
-    - page_context_word = 'issues'
   %li{class: ("active" if params[:state] == 'opened')}
     = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
-      #{state_filters_text_for(:opened, @project)}
+      #{issuables_state_counter_text(type, :opened)}
 
-  - if defined?(type) && type == :merge_requests
+  - if type == :merge_requests
     %li{class: ("active" if params[:state] == 'merged')}
       = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
-        #{state_filters_text_for(:merged, @project)}
+        #{issuables_state_counter_text(type, :merged)}
 
     %li{class: ("active" if params[:state] == 'closed')}
       = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
-        #{state_filters_text_for(:closed, @project)}
+        #{issuables_state_counter_text(type, :closed)}
   - else
     %li{class: ("active" if params[:state] == 'closed')}
       = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
-        #{state_filters_text_for(:closed, @project)}
+        #{issuables_state_counter_text(type, :closed)}
 
   %li{class: ("active" if params[:state] == 'all')}
     = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
-      #{state_filters_text_for(:all, @project)}
+      #{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 186963b32b87c5621351f8d8563b5ba693049791..2c89217cadd487b464c22a22f9b3dfa90ebee65b 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
-  = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+= form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-search-form') do
+  = search_field_tag :search, params[:search], { id: 'issuable_search', placeholder: 'Filter by name ...', class: 'form-control issuable_search search-text-input input-short', spellcheck: false }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 8e2fcbdfab8c24090ace207c8a4e9c8c53d63415..7363ead09ff775f92eb8b0864fff7557932aaacb 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,30 +107,31 @@
                   = dropdown_content do
                     .js-due-date-calendar
 
-      - if issuable.project.labels.any?
+      - if @labels && @labels.any?
+        - selected_labels = issuable.labels
         .block.labels
-          .sidebar-collapsed-icon
+          .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
             = icon('tags')
             %span
-              = issuable.labels_array.size
+              = selected_labels.size
           .title.hide-collapsed
             Labels
             = icon('spinner spin', class: 'block-loading')
             - if can_edit_issuable
               = link_to 'Edit', '#', class: 'edit-link pull-right'
-          .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
-            - if issuable.labels_array.any?
-              - issuable.labels_array.each do |label|
+          .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+            - if selected_labels.any?
+              - selected_labels.each do |label|
                 = link_to_label(label, type: issuable.to_ability_name)
             - else
               %span.no-value None
           .selectbox.hide-collapsed
-            - issuable.labels_array.each do |label|
+            - selected_labels.each do |label|
               = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
             .dropdown
-              %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
-                %span.dropdown-toggle-text
-                  Label
+              %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')
               .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
                 = render partial: "shared/issuable/label_page_default"
@@ -170,5 +171,5 @@
       new LabelsSelect();
       new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
       new Subscription('.subscription')
-      new DueDateSelect();
+      new gl.DueDateSelectors();
       sidebar = new Sidebar();
diff --git a/app/views/shared/issuable/form/_template_selector.html.haml b/app/views/shared/issuable/form/_template_selector.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d613bd31d81f19db2c657c4219b3f48aff4f21bf
--- /dev/null
+++ b/app/views/shared/issuable/form/_template_selector.html.haml
@@ -0,0 +1,13 @@
+- issuable = local_assigns.fetch(:issuable, nil)
+
+- return unless issuable && issuable_templates(issuable).any?
+
+.col-sm-3.col-lg-2
+  .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } }
+    = template_dropdown_tag(issuable) do
+      %ul.dropdown-footer-list
+        %li
+          %a.no-template
+            No template
+          %a.reset-template
+            Reset template
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
similarity index 75%
rename from app/views/projects/labels/_form.html.haml
rename to app/views/shared/labels/_form.html.haml
index aa143e54ffe4a8902be01b18c4579c42f2e059a2..647e05e5ff76ee2c4c5636380126d705184caf94 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
+= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
   = form_errors(@label)
 
   .form-group
@@ -14,7 +14,7 @@
     .col-sm-10
       .input-group
         .input-group-addon.label-color-preview &nbsp;
-        = f.color_field :color, class: "form-control"
+        = f.text_field :color, class: "form-control"
       .help-block
         Choose any color.
         %br
@@ -30,4 +30,4 @@
       = f.submit 'Save changes', class: 'btn btn-save js-save-button'
     - else
       = f.submit 'Create Label', class: 'btn btn-create js-save-button'
-    = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
+    = link_to 'Cancel', back_path, class: 'btn btn-cancel'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..1c0346bbc78f9269a1625ca540237e7f0937f438
--- /dev/null
+++ b/app/views/shared/members/_group.html.haml
@@ -0,0 +1,29 @@
+- group_link = local_assigns[:group_link]
+- group = group_link.group
+- can_admin_member = can?(current_user, :admin_project_member, @project)
+%li.member.group_member{ id: "group_member_#{group_link.id}" }
+  %span{ class: "list-item-name" }
+    = image_tag group_icon(group), class: "avatar s40", alt: ''
+    %strong
+      = link_to group.name, group_path(group)
+    .cgray
+      Joined #{time_ago_with_tooltip(group.created_at)}
+      - if group_link.expires?
+        ·
+        %span{ class: ('text-warning' if group_link.expires_soon?) }
+          Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
+  .controls.member-controls
+    = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
+      = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+      .prepend-left-5.clearable-input.member-form-control
+        = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
+        %i.clear-icon.js-clear-input
+    - if can_admin_member
+      = link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
+        remote: true,
+        method: :delete,
+        data: { confirm: "Are you sure you want to remove #{group.name}?" },
+        class: 'btn btn-remove prepend-left-10' do
+        %span.visible-xs-block
+          Delete
+        = icon('trash', class: 'hidden-xs')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index fc6e206d0821c01ddedc088c0669601cc29d2a34..432047a1c4ed6dd94fbfd947da4b3a2f79c7a0ba 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,64 +1,38 @@
 - show_roles = local_assigns.fetch(:show_roles, true)
 - show_controls = local_assigns.fetch(:show_controls, true)
-- user = member.user
+- user = local_assigns.fetch(:user, member.user)
+- source = member.source
+- can_admin_member = can?(current_user, action_member_permission(:update, member), member)
 
-%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
-  - if show_roles
-    .controls
-      %strong.control-text= member.human_access
-      - if show_controls
-        - if !user && can?(current_user, action_member_permission(:admin, member), member.source)
-          = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
-                    method: :post,
-                    class: 'btn'
-
-        - if can?(current_user, action_member_permission(:update, member), member)
-          = button_tag icon('pencil'),
-                       type: 'button',
-                       class: 'btn inline js-toggle-button',
-                       title: 'Edit access level'
-
-          - if member.request?
-            = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
-                      method: :post,
-                      class: 'btn btn-success',
-                      title: 'Grant access'
-
-        - if can?(current_user, action_member_permission(:destroy, member), member)
-          - if current_user == user
-            = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
-                      method: :delete,
-                      data: { confirm: leave_confirmation_message(member.source) },
-                      class: 'btn btn-remove'
-          - else
-            = link_to icon('trash'), member,
-                      remote: true,
-                      method: :delete,
-                      data: { confirm: remove_member_message(member) },
-                      class: 'btn btn-remove',
-                      title: remove_member_title(member)
-
-
-  %span{ class: ("list-item-name" if show_controls) }
+%li.member{ class: dom_class(member), id: dom_id(member) }
+  %span.list-item-name
     - if user
       = image_tag avatar_icon(user, 40), class: "avatar s40", alt: ''
       %strong
         = link_to user.name, user_path(user)
-      %span.cgray= user.username
+      %span.cgray= user.to_reference
 
       - if user == current_user
-        %span.label.label-success It's you
+        %span.label.label-success.prepend-left-5 It's you
 
       - if user.blocked?
         %label.label.label-danger
           %strong Blocked
 
-      .cgray
+      - if source.instance_of?(Group) && !@group
+        = link_to source, class: "member-group-link prepend-left-5" do
+          = "· #{source.name}"
+
+      .hidden-xs.cgray
         - if member.request?
           Requested
           = time_ago_with_tooltip(member.requested_at)
         - else
           Joined #{time_ago_with_tooltip(member.created_at)}
+        - if member.expires?
+          ·
+          %span{ class: ('text-warning' if member.expires_soon?) }
+            Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
 
     - else
       = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -69,12 +43,44 @@
           by
           = link_to member.created_by.name, user_path(member.created_by)
         = time_ago_with_tooltip(member.created_at)
-
   - if show_roles
-    .edit-member.hide.js-toggle-content
-      %br
-      = form_for member, remote: true do |f|
-        .prepend-top-10
-          = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
-        .prepend-top-10
-          = f.submit 'Save', class: 'btn btn-save btn-sm'
+    .controls.member-controls
+      - if show_controls
+        - if user != current_user
+          = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
+            = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+            .prepend-left-5.clearable-input.member-form-control
+              = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+              %i.clear-icon.js-clear-input
+        - else
+          %span.member-access-text= member.human_access
+
+        - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source)
+          = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
+                    method: :post,
+                    class: 'btn btn-default  prepend-left-10'
+
+        - elsif member.request? && can_admin_member
+          = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
+                    method: :post,
+                    class: 'btn btn-success prepend-left-10',
+                    title: 'Grant access'
+
+        - if can?(current_user, action_member_permission(:destroy, member), member)
+          - if current_user == user
+            = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
+                      method: :delete,
+                      data: { confirm: leave_confirmation_message(member.source) },
+                      class: 'btn btn-remove prepend-left-10'
+          - else
+            = link_to member,
+                      remote: true,
+                      method: :delete,
+                      data: { confirm: remove_member_message(member) },
+                      class: 'btn btn-remove prepend-left-10',
+                      title: remove_member_title(member) do
+              %span.visible-xs-block
+                Delete
+              = icon('trash', class: 'hidden-xs')
+      - else
+        %span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 40b39e850b00844cc1dd923452d6a6cde6567ef7..10050adfda5a4a59bbf8f369e4db0139da276300 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -1,8 +1,8 @@
 - if requesters.any?
   .panel.panel-default
     .panel-heading
+      Users requesting access to
       %strong= membership_source.name
-      access requests
       %span.badge= requesters.size
     %ul.content-list
       = render partial: 'shared/members/member', collection: requesters, as: :member
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 3c03c220ddda28bad006fa7429b63fbb418ef6e8..9e1b03794285f2436dffe00e5818238026412ce0 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -3,8 +3,9 @@
 - assignee = issuable.assignee
 - issuable_type = issuable.class.table_name
 - base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
 
-%li{ id: dom_id(issuable, 'sortable'),  class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'),  class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
   %span
     - if show_project_name
       %strong #{project.name} &middot;
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index b15e8ea73fe515b3bcf3ebb1c4ac3a4480a31825..33f93dccd3c4874ccfd108f170decaa2679cb6fa 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -8,7 +8,7 @@
           = link_to milestones_label_path(options) do
             - render_colored_label(label, tooltip: false)
         %span.prepend-description-left
-          = markdown(label.description, pipeline: :single_line)
+          = markdown_field(label, :description)
 
       .pull-info-right
         %span.append-right-20
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index acc3ccf4dcf1796b5dbb659b2115339dde7b62ab..3dccfb147bfaab5363c8842147ca6df4716eaa5f 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -33,7 +33,7 @@
   - if @project
     .row
       .col-sm-6= render('shared/milestone_expired', milestone: milestone)
-      .col-sm-6
+      .col-sm-6.milestone-actions
         - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
           = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
             Edit
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 7ff947a51db8ad8a13caa9fb055a8bb9b40f2581..548215243db5a55eb7145321e9044fd125ec9be1 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -26,7 +26,7 @@
 
 .detail-page-description.milestone-detail
   %h2.title
-    = markdown escape_once(milestone.title), pipeline: :single_line
+    = markdown_field(milestone, :title)
 
 - if milestone.complete?(current_user) && milestone.active?
   .alert.alert-success.prepend-top-default
@@ -55,4 +55,3 @@
             Open
         %td
           = ms.expires_at
-
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index ff1cf966a9b4caf200f9d09ebcc7db975a8fd8d7..feaa5570c219ba17bbebf8073308778a65212c67 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -11,7 +11,7 @@
               = icon("bell", class: "js-notification-loading")
               = notification_title(notification_setting.level)
             %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
-              %span.caret
+              = icon('caret-down')
               .sr-only Toggle dropdown
           - else
             %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b704981e3dbfdcabec388392611647077a7e53c9..a82fc95df84a92582f1cc1afb7549d5048c66ec5 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -27,5 +27,5 @@
                       %label{ for: field_id }
                         = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
                         %strong
-                          = event.to_s.humanize
+                          = notification_event_name(event)
                           = icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 281ec728e41477970cec4bff06bf7e4c17044dfb..264391fe84f6fd25403e7e9952a61e9e882cb7fc 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -20,11 +20,11 @@
       - if forks
         %span
           = icon('code-fork')
-          = project.forks_count
+          = number_with_delimiter(project.forks_count)
       - if stars
         %span
           = icon('star')
-          = project.star_count
+          = number_with_delimiter(project.star_count)
       %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
         = visibility_level_icon(project.visibility_level, fw: true)
 
@@ -32,10 +32,11 @@
       = link_to project_path(project), class: dom_class(project) do
         - if avatar
           .dash-project-avatar
-            - if use_creator_avatar
-              = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
-            - else
-              = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+            .avatar-container.s40
+              - if use_creator_avatar
+                = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+              - else
+                = project_icon(project, alt: '', class: 'avatar project-avatar s40')
         %span.project-full-name
           %span.namespace-name
             - if project.namespace && !skip_namespace
@@ -50,4 +51,4 @@
           class: "commit-row-message"
     - elsif project.description.present?
       .description
-        = markdown(project.description, pipeline: :description)
+        = markdown_field(project, :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 773ce8ac240f8bad822d8fc6a74f1b133cc2b18a..dcdba01aee9f4a12b9bf220cc802717e411082d9 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,9 +1,12 @@
 - unless @snippet.content.empty?
   - if markup?(@snippet.file_name)
     %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
-      = @snippet.data
+      = @snippet.content
     .file-content.wiki
-      = render_markup(@snippet.file_name, @snippet.data)
+      - if gitlab_markdown?(@snippet.file_name)
+        = preserve(markdown_field(@snippet, :content))
+      - else
+        = render_markup(@snippet.file_name, @snippet.content)
   - else
     = render 'shared/file_highlight', blob: @snippet
 - else
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 47ec09f62c604262456d2267470f7f916f8861a8..0c7880320208f9bda6bf6f5a6344e6632314480e 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,3 +1,7 @@
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('lib/ace.js')
+  = page_specific_javascript_tag('snippet/snippet_bundle.js')
+
 .snippet-form-holder
   = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
     = form_errors(@snippet)
@@ -31,8 +35,3 @@
       - else
         = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
 
-:javascript
-  var editor = ace.edit("editor");
-  $(".snippet-form-holder form").submit(function(){
-    $(".snippet-file-content").val(editor.getValue());
-  });
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index af753496260b1c0e210cee2a3dddb4703c23ec7b..d7506e07ff6b0663445658d2e14eaab029cc60cc 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -6,12 +6,13 @@
   %strong.item-title
     Snippet #{@snippet.to_reference}
   %span.creator
-    created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")}
+    authored
     = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
     - if @snippet.updated_at != @snippet.created_at
       %span
         = icon('edit', title: 'edited')
         = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
+    by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
 
   .snippet-actions
     - if @snippet.project_id?
@@ -19,6 +20,5 @@
     - else
       = render "snippets/actions"
 
-.content-block.second-block
-  %h2.snippet-title.prepend-top-0.append-bottom-0
-    = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+%h2.snippet-title.prepend-top-0.append-bottom-0
+  = markdown_field(@snippet, :title)
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c96dfefe17f5d21e0634714c9cfdf2e35687aab5..ea17bec8677ed48680cbb7c8abbdaebfda7396f5 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -3,19 +3,30 @@
 
   .title
     = link_to reliable_snippet_path(snippet) do
-      = truncate(snippet.title, length: 60)
+      = snippet.title
       - if snippet.private?
-        %span.label.label-gray
+        %span.label.label-gray.hidden-xs
           = icon('lock')
           private
-    %span.monospace.pull-right
+    %span.monospace.pull-right.hidden-xs
       = snippet.file_name
 
-  %small.pull-right.cgray
+    %ul.controls.visible-xs
+      %li
+        - note_count = snippet.notes.user.count
+        = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+          = icon('comments')
+          = note_count
+      %li
+        %span.sr-only
+          = visibility_level_label(snippet.visibility_level)
+        = visibility_level_icon(snippet.visibility_level, fw: false)
+
+  %small.pull-right.cgray.hidden-xs
     - if snippet.project_id?
       = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
 
-  .snippet-info
+  .snippet-info.hidden-xs
     = link_to user_snippets_path(snippet.author) do
       = snippet.author_name
     authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d2ec6c3ddef72ad2cf0c86bc553063401410d4db..5d659eb83a9c3993ce002b9082e9e377c3f8ff84 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -51,6 +51,13 @@
                 %strong Issues events
               %p.light
                 This URL will be triggered when an issue is created/updated/merged
+          %li
+            = f.check_box :confidential_issues_events, class: 'pull-left'
+            .prepend-left-20
+              = f.label :confidential_issues_events, class: 'list-label' do
+                %strong Confidential Issues events
+              %p.light
+                This URL will be triggered when a confidential issue is created/updated/merged
           %li
             = f.check_box :merge_requests_events, class: 'pull-left'
             .prepend-left-20
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 160c6cd84da168dc74229839b0807489247b2b77..1d0e549ed3d64ec891ec5fcb5b90481c66758c89 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,28 +1,28 @@
 .hidden-xs
   - if current_user
-    = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do
-      New Snippet
-  - if can?(current_user, :update_personal_snippet, @snippet)
-    = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
-      Edit
+    = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
+      New snippet
   - if can?(current_user, :admin_personal_snippet, @snippet)
     = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
       Delete
+  - if can?(current_user, :update_personal_snippet, @snippet)
+    = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+      Edit
 - if current_user
   .visible-xs-block.dropdown
     %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
       Options
-      %span.caret
+      = icon('caret-down')
     .dropdown-menu.dropdown-menu-full-width
       %ul
         %li
-          = link_to new_snippet_path, title: "New Snippet" do
-            New Snippet
-        - if can?(current_user, :update_personal_snippet, @snippet)
-          %li
-            = link_to edit_snippet_path(@snippet) do
-              Edit
+          = link_to new_snippet_path, title: "New snippet" do
+            New snippet
         - if can?(current_user, :admin_personal_snippet, @snippet)
           %li
             = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
               Delete
+        - if can?(current_user, :update_personal_snippet, @snippet)
+          %li
+            = link_to edit_snippet_path(@snippet) do
+              Edit
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 80a3e731e1df9e3c38b09180d8665214d0856565..77b66ca74b6385ac55c3b9d8dc22e4d2c41beb21 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,7 +1,13 @@
-%ul.content-list
-  = render partial: 'shared/snippets/snippet', collection: @snippets
-  - if @snippets.empty?
-    %li
-      .nothing-here-block Nothing here.
+- remote = local_assigns.fetch(:remote, false)
 
-= paginate @snippets, theme: 'gitlab'
+.snippets-list-holder
+  %ul.content-list
+    = render partial: 'shared/snippets/snippet', collection: @snippets
+    - if @snippets.empty?
+      %li
+        .nothing-here-block Nothing here.
+
+  = paginate @snippets, theme: 'gitlab', remote: remote
+
+:javascript
+  gl.SnippetsList();
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index ed3992650d4a0c741f809ab4f80e49e5cde3c29c..27d7a6c5bb67a202c55fa1f49304edaa6ef211da 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,13 +1,15 @@
 - page_title @snippet.title, "Snippets"
 
-.snippet-holder
-  = render 'shared/snippets/header'
+= render 'shared/snippets/header'
 
-  %article.file-holder.file-holder-no-border.snippet-file-content
-    .file-title.file-title-clear
-      = blob_icon 0, @snippet.file_name
-      = @snippet.file_name
-      .file-actions.hidden-xs
-        = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
-        = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
-    = render 'shared/snippets/blob'
+%article.file-holder.snippet-file-content
+  .file-title
+    = blob_icon 0, @snippet.file_name
+    = @snippet.file_name
+    .file-actions
+      = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+      = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+      = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
+  = render 'shared/snippets/blob'
+
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 75fb0e303ada8cb196d366576b5c574e78960ea4..232ca26c1af0a60faf0e2f99bf7d5a8300315736 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -6,7 +6,7 @@
 %script#js-authenticate-u2f-setup{ type: "text/template" }
   %div
     %p Insert your security key (if you haven't already), and press the button below.
-    %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
+    %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
 
 %script#js-authenticate-u2f-in-progress{ type: "text/template" }
   %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
@@ -20,6 +20,8 @@
   %div
     %p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
     = form_tag(new_user_session_path, method: :post) do |f|
+      - resource_params = params[resource_name].presence || params
+      = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
       = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
       = submit_tag "Authenticate via U2F Device", class: "btn btn-success"
 
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index cbb8dfb78296b940b85c6d0f5de63e0b11c97284..8f7b42eb351f4e34aee018029474bac896dc1792 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -28,10 +28,15 @@
 
 %script#js-register-u2f-registered{ type: "text/template" }
   %div.row.append-bottom-10
-    %p Your device was successfully set up! Click this button to register with the GitLab server.
-    = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
-      = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
-      = submit_tag "Register U2F Device", class: "btn btn-success"
+    .col-md-12
+      %p Your device was successfully set up! Give it a name and register it with the GitLab server.
+      = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
+        .row.append-bottom-10
+          .col-md-3
+            = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
+          .col-md-3
+            = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
+            = submit_tag "Register U2F Device", class: "btn btn-success"
 
 :javascript
   var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index f360fbb3d5d94159871f64e52cf0b9dae86a4886..eff6c80d1442b93c3bab6dc628f3309d81902ec4 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -1,4 +1,5 @@
 .clearfix
   - groups.each do |group|
     = link_to group, class: 'profile-groups-avatars inline', title: group.name do
-      = image_tag group_icon(group), class: 'avatar group-avatar s40'
+      .avatar-container.s40
+        = image_tag group_icon(group), class: 'avatar group-avatar s40'
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 77f2ddefb1e28bff028e29c1cf807913363b222e..09ff8a76d2711da184e8b7e7b10bf74064607039 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -4,6 +4,6 @@
     Summary of issues, merge requests, and push events
 :javascript
   new Calendar(
-    #{@timestamps.to_json},
+    #{@activity_dates.to_json},
     '#{user_calendar_activities_path}'
-  );
+  );
\ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c7f39868e71f5487fd052d89bfbc3ab2c93215d2..1e0752bd3c3925abb7f9669872cded2a9e66017f 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -10,75 +10,79 @@
   = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
 
 .user-profile
-  .cover-block
+  .cover-block.user-cover-block
     .cover-controls
       - if @user == current_user
         = link_to profile_path, class: 'btn btn-gray' do
           = icon('pencil')
       - elsif current_user
-        %span.report-abuse
-          - if @user.abuse_report
-            %button.btn.btn-danger{ title: 'Already reported for abuse',
-              data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
-              = icon('exclamation-circle')
-          - else
-            = 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: 'left', container: 'body'} do
-              = icon('exclamation-circle')
+        - if @user.abuse_report
+          %button.btn.btn-danger{ title: 'Already reported for abuse',
+            data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+            = icon('exclamation-circle')
+        - else
+          = 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
-        &nbsp;
         = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
           = icon('rss')
         - if current_user.admin?
-          &nbsp;
           = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
             data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
             = icon('users')
 
-    .avatar-holder
-      = link_to avatar_icon(@user, 400), target: '_blank' do
-        = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
-    .cover-title
-      = @user.name
-
-    .cover-desc
-      %span.middle-dot-divider
-        @#{@user.username}
-      %span.middle-dot-divider
-        Member since #{@user.created_at.to_s(:medium)}
+    .profile-header
+      .avatar-holder
+        = link_to avatar_icon(@user, 400), target: '_blank' do
+          = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+
+      .user-info
+        .cover-title
+          = @user.name
+
+        .cover-desc.member-date
+          %span.middle-dot-divider
+            @#{@user.username}
+          %span.middle-dot-divider
+            Member since #{@user.created_at.to_s(:medium)}
+
+        .cover-desc
+          - unless @user.public_email.blank?
+            .profile-link-holder.middle-dot-divider
+              = link_to @user.public_email, "mailto:#{@user.public_email}"
+          - unless @user.skype.blank?
+            .profile-link-holder.middle-dot-divider
+              = link_to "skype:#{@user.skype}", title: "Skype" do
+                = icon('skype')
+          - unless @user.linkedin.blank?
+            .profile-link-holder.middle-dot-divider
+              = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+                = icon('linkedin-square')
+          - unless @user.twitter.blank?
+            .profile-link-holder.middle-dot-divider
+              = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+                = icon('twitter-square')
+          - unless @user.website_url.blank?
+            .profile-link-holder.middle-dot-divider
+              = link_to @user.short_website_url, @user.full_website_url
+          - unless @user.location.blank?
+            .profile-link-holder.middle-dot-divider
+              = icon('map-marker')
+              = @user.location
+          - unless @user.organization.blank?
+            .profile-link-holder.middle-dot-divider
+              = icon('building')
+              = @user.organization
 
     - if @user.bio.present?
       .cover-desc
         %p.profile-user-bio
           = @user.bio
 
-    .cover-desc
-      - unless @user.public_email.blank?
-        .profile-link-holder.middle-dot-divider
-          = link_to @user.public_email, "mailto:#{@user.public_email}"
-      - unless @user.skype.blank?
-        .profile-link-holder.middle-dot-divider
-          = link_to "skype:#{@user.skype}", title: "Skype" do
-            = icon('skype')
-      - unless @user.linkedin.blank?
-        .profile-link-holder.middle-dot-divider
-          = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
-            = icon('linkedin-square')
-      - unless @user.twitter.blank?
-        .profile-link-holder.middle-dot-divider
-          = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
-            = icon('twitter-square')
-      - unless @user.website_url.blank?
-        .profile-link-holder.middle-dot-divider
-          = link_to @user.short_website_url, @user.full_website_url
-      - unless @user.location.blank?
-        .profile-link-holder.middle-dot-divider
-          = icon('map-marker')
-          = @user.location
-
     %ul.nav-links.center.user-profile-nav
       %li.js-activity-tab
-        = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+        = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
           Activity
       %li.js-groups-tab
         = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
@@ -123,6 +127,6 @@
 :javascript
   var userProfile;
 
-  userProfile = new User({
+  userProfile = new gl.User({
     action: "#{controller.action_name}"
   });
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 667fff031dd34f788ddbdc5983c178e63acdcd13..c2dc955b27c1eedb4612807ab52db79cd7fa7d98 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,7 +1,6 @@
 class AdminEmailWorker
   include Sidekiq::Worker
-
-  sidekiq_options retry: false # this job auto-repeats via sidekiq-cron
+  include CronjobQueue
 
   def perform
     repository_check_failed_count = Project.where(last_repository_check_failed: true).count
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..def0ab1dde12a9be9b95be014bbabb27558880c3
--- /dev/null
+++ b/app/workers/build_coverage_worker.rb
@@ -0,0 +1,9 @@
+class BuildCoverageWorker
+  include Sidekiq::Worker
+  include BuildQueue
+
+  def perform(build_id)
+    Ci::Build.find_by(id: build_id)
+      .try(:update_coverage)
+  end
+end
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
index 1c7a04a66a819e59c350cba67eb35ac6b19549af..5fdb1f2baa05fce65de38baf8707fb11099d87e9 100644
--- a/app/workers/build_email_worker.rb
+++ b/app/workers/build_email_worker.rb
@@ -1,5 +1,6 @@
 class BuildEmailWorker
   include Sidekiq::Worker
+  include BuildQueue
 
   def perform(build_id, recipients, push_data)
     recipients.each do |recipient|
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..466410bf08ce512a304d37f380fbfa617354d1e5
--- /dev/null
+++ b/app/workers/build_finished_worker.rb
@@ -0,0 +1,11 @@
+class BuildFinishedWorker
+  include Sidekiq::Worker
+  include BuildQueue
+
+  def perform(build_id)
+    Ci::Build.find_by(id: build_id).try do |build|
+      BuildCoverageWorker.new.perform(build.id)
+      BuildHooksWorker.new.perform(build.id)
+    end
+  end
+end
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9965af935d4b2a63fe12ff2ef815b0544a445890
--- /dev/null
+++ b/app/workers/build_hooks_worker.rb
@@ -0,0 +1,9 @@
+class BuildHooksWorker
+  include Sidekiq::Worker
+  include BuildQueue
+
+  def perform(build_id)
+    Ci::Build.find_by(id: build_id)
+      .try(:execute_hooks)
+  end
+end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0ad52686649d31a9c47dcae7458cb83de45c241
--- /dev/null
+++ b/app/workers/build_success_worker.rb
@@ -0,0 +1,27 @@
+class BuildSuccessWorker
+  include Sidekiq::Worker
+  include BuildQueue
+
+  def perform(build_id)
+    Ci::Build.find_by(id: build_id).try do |build|
+      create_deployment(build)
+    end
+  end
+
+  private
+
+  def create_deployment(build)
+    return if build.environment.blank?
+
+    service = CreateDeploymentService.new(
+      build.project, build.user,
+      environment: build.environment,
+      sha: build.sha,
+      ref: build.ref,
+      tag: build.tag,
+      options: build.options.to_h[:environment],
+      variables: build.variables)
+
+    service.execute(build)
+  end
+end
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c4cb4733482cdf00e9a422e5eb9a3b2d2c195702
--- /dev/null
+++ b/app/workers/clear_database_cache_worker.rb
@@ -0,0 +1,24 @@
+# This worker clears all cache fields in the database, working in batches.
+class ClearDatabaseCacheWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  BATCH_SIZE = 1000
+
+  def perform
+    CacheMarkdownField.caching_classes.each do |kls|
+      fields = kls.cached_markdown_fields.html_fields
+      clear_cache_fields = fields.each_with_object({}) do |field, memo|
+        memo[field] = nil
+      end
+
+      Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
+
+      kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
+        relation.update_all(clear_cache_fields)
+      end
+    end
+
+    nil
+  end
+end
diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf0ead40a8beab13e47b471fdb6ba9819e46d737
--- /dev/null
+++ b/app/workers/concerns/build_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI build workers.
+module BuildQueue
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_options queue: :build
+  end
+end
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e918bb011e01370ea4717de621c8e10d11b2e9f6
--- /dev/null
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets various Sidekiq settings for workers executed using a
+# cronjob.
+module CronjobQueue
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_options queue: :cronjob, retry: false
+  end
+end
diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..132bae6022b4c1da9bb22ba986ab139049c5d00a
--- /dev/null
+++ b/app/workers/concerns/dedicated_sidekiq_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets the queue of a Sidekiq worker based on the worker's class
+# name/namespace.
+module DedicatedSidekiqQueue
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_')
+  end
+end
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca3860e1d389d2a77dac12adb345e19b5bfe4990
--- /dev/null
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI pipeline workers.
+module PipelineQueue
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_options queue: :pipeline
+  end
+end
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a597321ccf45a217ee29d5a5a386c936eba684a1
--- /dev/null
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various repository check workers.
+module RepositoryCheckQueue
+  extend ActiveSupport::Concern
+
+  included do
+    sidekiq_options queue: :repository_check, retry: false
+  end
+end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 6ff361e4d8009f392f901465f14bae774aa01a09..3194c389b3d40bcee73d4285d1eaf25c9d5ecc31 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,5 +1,6 @@
 class DeleteUserWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
   def perform(current_user_id, delete_user_id, options = {})
     delete_user  = User.find(delete_user_id)
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 842eebdea9e2e63ec826daa1487357f517c6cfe0..d3f7e479a8d7ec1268b0952949d5c59af84ff6f7 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,7 +1,6 @@
 class EmailReceiverWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :incoming_email
+  include DedicatedSidekiqQueue
 
   def perform(raw)
     return unless Gitlab::IncomingEmail.enabled?
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index c6a5af2809a1232a276d80e37c9529c388eb3e02..b9cd49985dcd650f98b273b351fd3c88b69e9dfe 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,7 +1,7 @@
 class EmailsOnPushWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
-  sidekiq_options queue: :mailers
   attr_reader :email, :skip_premailer
 
   def perform(project_id, recipients, push_data, options = {})
@@ -33,13 +33,13 @@ class EmailsOnPushWorker
     reverse_compare = false
 
     if action == :push
-      compare = CompareService.new.execute(project, before_sha, project, after_sha)
+      compare = CompareService.new.execute(project, after_sha, project, before_sha)
       diff_refs = compare.diff_refs
 
       return false if compare.same
 
       if compare.commits.empty?
-        compare = CompareService.new.execute(project, after_sha, project, before_sha)
+        compare = CompareService.new.execute(project, before_sha, project, after_sha)
         diff_refs = compare.diff_refs
 
         reverse_compare = true
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index c64ea108d52ef58067f1eb81d84ca4687a6e4f2c..a27585fd3897aa1fe9fb9ce9885bfc77b77e4d99 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,13 +1,13 @@
 class ExpireBuildArtifactsWorker
   include Sidekiq::Worker
+  include CronjobQueue
 
   def perform
-    Rails.logger.info 'Cleaning old build artifacts'
+    Rails.logger.info 'Scheduling removal of build artifacts'
 
-    builds = Ci::Build.with_expired_artifacts
-    builds.find_each(batch_size: 50).each do |build|
-      Rails.logger.debug "Removing artifacts build #{build.id}..."
-      build.erase_artifacts!
-    end
+    build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
+    build_ids = build_ids.map { |build_id| [build_id] }
+
+    Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids )
   end
 end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb403c134d138d36d63bd3aaaadf6d6860efdd13
--- /dev/null
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -0,0 +1,16 @@
+class ExpireBuildInstanceArtifactsWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  def perform(build_id)
+    build = Ci::Build
+      .with_expired_artifacts
+      .reorder(nil)
+      .find_by(id: build_id)
+
+    return unless build.try(:project)
+
+    Rails.logger.info "Removing artifacts for build #{build.id}..."
+    build.erase_artifacts!
+  end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index a6cefd4d60175afa3231dd340090627766df2567..d369b639ae9949df6ae0b5cf50ad7f675f62eded 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,16 +1,58 @@
 class GitGarbageCollectWorker
   include Sidekiq::Worker
-  include Gitlab::ShellAdapter
+  include DedicatedSidekiqQueue
+  include Gitlab::CurrentSettings
 
-  sidekiq_options queue: :gitlab_shell, retry: false
+  sidekiq_options retry: false
 
-  def perform(project_id)
+  def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
     project = Project.find(project_id)
+    task = task.to_sym
+
+    cmd = command(task)
+    repo_path = project.repository.path_to_repo
+    description = "'#{cmd.join(' ')}' in #{repo_path}"
+
+    Gitlab::GitLogger.info(description)
+
+    output, status = Gitlab::Popen.popen(cmd, repo_path)
+    Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
 
-    gitlab_shell.gc(project.repository_storage_path, project.path_with_namespace)
     # Refresh the branch cache in case garbage collection caused a ref lookup to fail
+    flush_ref_caches(project) if task == :gc
+  ensure
+    Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
+  end
+
+  private
+
+  def command(task)
+    case task
+    when :gc
+      git(write_bitmaps: bitmaps_enabled?) + %w[gc]
+    when :full_repack
+      git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects]
+    when :incremental_repack
+      # Normal git repack fails when bitmaps are enabled. It is impossible to
+      # create a bitmap here anyway.
+      git(write_bitmaps: false) + %w[repack -d]
+    else
+      raise "Invalid gc task: #{task.inspect}"
+    end
+  end
+
+  def flush_ref_caches(project)
     project.repository.after_create_branch
     project.repository.branch_names
     project.repository.has_visible_content?
   end
+
+  def bitmaps_enabled?
+    current_application_settings.housekeeping_bitmaps_enabled
+  end
+
+  def git(write_bitmaps:)
+    config_value = write_bitmaps ? 'true' : 'false'
+    %W[git -c repack.writeBitmaps=#{config_value}]
+  end
 end
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index cfeda88bbc5c6ceaef5d19215cf32b1dc77aa131..964287a1793a03d7f27271aa2f3019329ae6e6e0 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,8 +1,7 @@
 class GitlabShellWorker
   include Sidekiq::Worker
   include Gitlab::ShellAdapter
-
-  sidekiq_options queue: :gitlab_shell
+  include DedicatedSidekiqQueue
 
   def perform(action, *arg)
     gitlab_shell.send(action, *arg)
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 5048746f09ba5590f9a8b1a10318726345b112dc..a49a5fd08557ff877a1fa34051d040ad19d0d711 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,7 +1,6 @@
 class GroupDestroyWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include DedicatedSidekiqQueue
 
   def perform(group_id, user_id)
     begin
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 72e3a9ae734686fe50e65a541b66d9a2bc7dfecb..7957ed807ab07696915e3716b632c2c0c7863d33 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,7 +1,6 @@
 class ImportExportProjectCleanupWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include CronjobQueue
 
   def perform
     ImportExportCleanUpService.new.execute
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 19f38358eb51171951cfde71f4b4e494cd9511ad..7e44b24174358f269bc40e48b44950560a9bb9c8 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -3,6 +3,7 @@ require 'socket'
 
 class IrkerWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
   def perform(project_id, chans, colors, push_data, settings)
     project = Project.find(project_id)
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index c87c0a252b1f56f6489c4cf505ccd5e91f86e2c2..79efca4f2f9443f18e4eb45aa0236f1143bc9358 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,7 +1,6 @@
 class MergeWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include DedicatedSidekiqQueue
 
   def perform(merge_request_id, current_user_id, params)
     params = params.with_indifferent_access
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 1b3232cd36521d62fbe3d9dcc726b62d23d221a2..c3e62bb88c0191a1714bd375f0c195d0463f3002 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,7 +1,6 @@
 class NewNoteWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include DedicatedSidekiqQueue
 
   def perform(note_id, note_params)
     note = Note.find(note_id)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e36eacebf88cd5d231b13311ed1991e3e695e3c
--- /dev/null
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -0,0 +1,9 @@
+class PipelineHooksWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id)
+    Ci::Pipeline.find_by(id: pipeline_id)
+      .try(:execute_hooks)
+  end
+end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34f6ef161fb3e2e63c01f954b162e642ce2e41f7
--- /dev/null
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -0,0 +1,29 @@
+class PipelineMetricsWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id)
+    Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+      update_metrics_for_active_pipeline(pipeline) if pipeline.active?
+      update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success?
+    end
+  end
+
+  private
+
+  def update_metrics_for_active_pipeline(pipeline)
+    metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+  end
+
+  def update_metrics_for_succeeded_pipeline(pipeline)
+    metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+  end
+
+  def metrics(pipeline)
+    MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline))
+  end
+
+  def merge_requests(pipeline)
+    pipeline.merge_requests.map(&:id)
+  end
+end
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cdb860b66758056253cff38cfb2883cbfe620d41
--- /dev/null
+++ b/app/workers/pipeline_notification_worker.rb
@@ -0,0 +1,12 @@
+class PipelineNotificationWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id, recipients = nil)
+    pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+
+    return unless pipeline
+
+    NotificationService.new.pipeline_finished(pipeline, recipients)
+  end
+end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..357e4a9a1c3aedada45058581af723f0bf5b7e9c
--- /dev/null
+++ b/app/workers/pipeline_process_worker.rb
@@ -0,0 +1,9 @@
+class PipelineProcessWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id)
+    Ci::Pipeline.find_by(id: pipeline_id)
+      .try(:process!)
+  end
+end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2aa6fff24da1878615bad011dfcc9e4a2f9942e1
--- /dev/null
+++ b/app/workers/pipeline_success_worker.rb
@@ -0,0 +1,12 @@
+class PipelineSuccessWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id)
+    Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+      MergeRequests::MergeWhenBuildSucceedsService
+        .new(pipeline.project, nil)
+        .trigger(pipeline)
+    end
+  end
+end
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96c4152c674c50395240cbd55ea34cce9ce6c31c
--- /dev/null
+++ b/app/workers/pipeline_update_worker.rb
@@ -0,0 +1,9 @@
+class PipelineUpdateWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id)
+    Ci::Pipeline.find_by(id: pipeline_id)
+      .try(:update_status)
+  end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index a9a2b7160059dcda26948dc1ce9201abfc657053..2fff6b0105d1949c868517da72dbc6b8656545ea 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,7 +1,6 @@
 class PostReceive
   include Sidekiq::Worker
-
-  sidekiq_options queue: :post_receive
+  include DedicatedSidekiqQueue
 
   def perform(repo_path, identifier, changes)
     if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
@@ -17,7 +16,7 @@ class PostReceive
     post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
 
     if post_received.project.nil?
-      log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
+      log("Triggered hook for non-existing project with full path \"#{repo_path}\"")
       return false
     end
 
@@ -26,7 +25,7 @@ class PostReceive
     elsif post_received.regular_project?
       process_project_changes(post_received)
     else
-      log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+      log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"")
       false
     end
   end
@@ -38,7 +37,7 @@ class PostReceive
       @user ||= post_received.identify(newrev)
 
       unless @user
-        log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
+        log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
         return false
       end
 
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..071741fbacd2cd6e6aca6860b9731f9a3040cc31
--- /dev/null
+++ b/app/workers/process_commit_worker.rb
@@ -0,0 +1,67 @@
+# Worker for processing individiual commit messages pushed to a repository.
+#
+# Jobs for this worker are scheduled for every commit that is being pushed. As a
+# result of this the workload of this worker should be kept to a bare minimum.
+# Consider using an extra worker if you need to add any extra (and potentially
+# slow) processing of commits.
+class ProcessCommitWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  # project_id - The ID of the project this commit belongs to.
+  # user_id - The ID of the user that pushed the commit.
+  # commit_sha - The SHA1 of the commit to process.
+  # default - The data was pushed to the default branch.
+  def perform(project_id, user_id, commit_sha, default = false)
+    project = Project.find_by(id: project_id)
+
+    return unless project
+
+    user = User.find_by(id: user_id)
+
+    return unless user
+
+    commit = find_commit(project, commit_sha)
+
+    return unless commit
+
+    author = commit.author || user
+
+    process_commit_message(project, commit, user, author, default)
+
+    update_issue_metrics(commit, author)
+  end
+
+  def process_commit_message(project, commit, user, author, default = false)
+    closed_issues = default ? commit.closes_issues(user) : []
+
+    unless closed_issues.empty?
+      close_issues(project, user, author, commit, closed_issues)
+    end
+
+    commit.create_cross_references!(author, closed_issues)
+  end
+
+  def close_issues(project, user, author, commit, issues)
+    # We don't want to run permission related queries for every single issue,
+    # therefor we use IssueCollection here and skip the authorization check in
+    # Issues::CloseService#execute.
+    IssueCollection.new(issues).updatable_by_user(user).each do |issue|
+      Issues::CloseService.new(project, author).
+        close_issue(issue, commit: commit)
+    end
+  end
+
+  def update_issue_metrics(commit, author)
+    mentioned_issues = commit.all_references(author).issues
+
+    Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+      update_all(first_mentioned_in_commit_at: commit.committed_date)
+  end
+
+  private
+
+  def find_commit(project, sha)
+    project.commit(sha)
+  end
+end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index ccefd0f71a0dfd762831b4719e1ce0a588dc0330..4dfa745fb509303562a7bc6f7ddbceef90357b7d 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,9 +1,41 @@
+# Worker for updating any project specific caches.
+#
+# This worker runs at most once every 15 minutes per project. This is to ensure
+# that multiple instances of jobs for this worker don't hammer the underlying
+# storage engine as much.
 class ProjectCacheWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
-  sidekiq_options queue: :default
+  LEASE_TIMEOUT = 15.minutes.to_i
+
+  def self.lease_for(project_id)
+    Gitlab::ExclusiveLease.
+      new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
+  end
+
+  # Overwrite Sidekiq's implementation so we only schedule when actually needed.
+  def self.perform_async(project_id)
+    # If a lease for this project is still being held there's no point in
+    # scheduling a new job.
+    super unless lease_for(project_id).exists?
+  end
 
   def perform(project_id)
+    if try_obtain_lease_for(project_id)
+      Rails.logger.
+        info("Obtained ProjectCacheWorker lease for project #{project_id}")
+    else
+      Rails.logger.
+        info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
+
+      return
+    end
+
+    update_caches(project_id)
+  end
+
+  def update_caches(project_id)
     project = Project.find(project_id)
 
     return unless project.repository.exists?
@@ -15,4 +47,8 @@ class ProjectCacheWorker
       project.repository.build_cache
     end
   end
+
+  def try_obtain_lease_for(project_id)
+    self.class.lease_for(project_id).try_obtain
+  end
 end
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 3062301a9b1fcf63cb91f7969fe8fd2d105f53db..b462327490ef4715741876ad3106a28cd718c996 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,7 +1,6 @@
 class ProjectDestroyWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include DedicatedSidekiqQueue
 
   def perform(project_id, user_id, params)
     begin
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 615311e63f50efaef365bbf8e028b49a669b34ab..6009aa1b191bf079bc89bf91c978e484a4dc0075 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,7 +1,8 @@
 class ProjectExportWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
-  sidekiq_options queue: :gitlab_shell, retry: 3
+  sidekiq_options retry: 3
 
   def perform(current_user_id, project_id)
     current_user = User.find(current_user_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 64d39c4d3f7ef436cb4ee49e2a2d4d9a5a5ee72a..fdfdeab7b4159ca5dc3ac0042e928ad6aab975a9 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,7 +1,6 @@
 class ProjectServiceWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :project_web_hook
+  include DedicatedSidekiqQueue
 
   def perform(hook_id, data)
     data = data.with_indifferent_access
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb
index fb87896528889c245185fb95adb1761612bea26a..d973e662ff22d07f34b48fff95442043187ffe2f 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/project_web_hook_worker.rb
@@ -1,7 +1,8 @@
 class ProjectWebHookWorker
   include Sidekiq::Worker
+  include DedicatedSidekiqQueue
 
-  sidekiq_options queue: :project_web_hook
+  sidekiq_options retry: 4
 
   def perform(hook_id, data, hook_name)
     data = data.with_indifferent_access
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..392abb9c21b43e2476ee37362b3ae7ebc5a09659
--- /dev/null
+++ b/app/workers/prune_old_events_worker.rb
@@ -0,0 +1,18 @@
+class PruneOldEventsWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  def perform
+    # Contribution calendar shows maximum 12 months of events.
+    # Double nested query is used because MySQL doesn't allow DELETE subqueries
+    # on the same table.
+    Event.unscoped.where(
+      '(id IN (SELECT id FROM (?) ids_to_remove))',
+      Event.unscoped.where(
+        'created_at < ?',
+        (12.months + 1.day).ago).
+      select(:id).
+      limit(10_000)).
+    delete_all
+  end
+end
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a619f834100cf278d74dab2a81482673a425069
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,8 @@
+class RemoveExpiredGroupLinksWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  def perform
+    ProjectGroupLink.expired.destroy_all
+  end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..31f652e5f9bc11f177393ff2c9f3761adfb17d20
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,14 @@
+class RemoveExpiredMembersWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  def perform
+    Member.expired.find_each do |member|
+      begin
+        Members::AuthorizedDestroyService.new(member).execute
+      rescue => ex
+        logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+      end
+    end
+  end
+end
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b80f131d5f7cd445703cef508b6ed9545cac8995
--- /dev/null
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -0,0 +1,8 @@
+class RemoveUnreferencedLfsObjectsWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  def perform
+    LfsObject.destroy_unreferenced
+  end
+end
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index a2e49c61f59543d917dbdfff4311ca9c445124d8..e47069df189069efb8ddf2f20c718242d30c9c62 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,7 +1,6 @@
 class RepositoryArchiveCacheWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include CronjobQueue
 
   def perform
     RepositoryArchiveCleanUpService.new.execute
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index a3e16fa5212dff6f273638ebcf0e9ff85dd57852..c3e7491ec4ec9f1f937c9c7c9ee1dc97d16f9bf5 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,14 +1,13 @@
 module RepositoryCheck
   class BatchWorker
     include Sidekiq::Worker
-  
+    include CronjobQueue
+
     RUN_TIME = 3600
-  
-    sidekiq_options retry: false
-  
+
     def perform
       start = Time.now
-  
+
       # This loop will break after a little more than one hour ('a little
       # more' because `git fsck` may take a few minutes), or if it runs out of
       # projects to check. By default sidekiq-cron will start a new
@@ -17,15 +16,15 @@ module RepositoryCheck
       project_ids.each do |project_id|
         break if Time.now - start >= RUN_TIME
         break unless current_settings.repository_checks_enabled
-  
+
         next unless try_obtain_lease(project_id)
-  
+
         SingleRepositoryWorker.new.perform(project_id)
       end
     end
-  
+
     private
-  
+
     # Project.find_each does not support WHERE clauses and
     # Project.find_in_batches does not support ordering. So we just build an
     # array of ID's. This is OK because we do it only once an hour, because
@@ -39,7 +38,7 @@ module RepositoryCheck
         reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
       never_checked_projects + old_check_projects
     end
-  
+
     def try_obtain_lease(id)
       # Use a 24-hour timeout because on servers/projects where 'git fsck' is
       # super slow we definitely do not want to run it twice in parallel.
@@ -48,7 +47,7 @@ module RepositoryCheck
         timeout: 24.hours
       ).try_obtain
     end
-  
+
     def current_settings
       # No caching of the settings! If we cache them and an admin disables
       # this feature, an active RepositoryCheckWorker would keep going for up
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index b7202ddff34473cdeb24ca753b639718da8c3540..1f1b38540eeef0d0acc5141b9b1939fb7f1bcdf4 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,8 +1,7 @@
 module RepositoryCheck
   class ClearWorker
     include Sidekiq::Worker
-
-    sidekiq_options retry: false
+    include RepositoryCheckQueue
 
     def perform
       # Do small batched updates because these updates will be slow and locking
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 98ddf5d06884ef6f6c15e53b092e461474342f18..3d8bfc6fc6c4d1af52191cf0eb325c29101609e0 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,8 +1,7 @@
 module RepositoryCheck
   class SingleRepositoryWorker
     include Sidekiq::Worker
-
-    sidekiq_options retry: false
+    include RepositoryCheckQueue
 
     def perform(project_id)
       project = Project.find(project_id)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index d69d6037053d3da556ab1b80fde4a75a529706b9..efc99ec962a96d86960a7e4645ce1ee8a0e2b10e 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,10 +1,13 @@
 class RepositoryForkWorker
   include Sidekiq::Worker
   include Gitlab::ShellAdapter
-
-  sidekiq_options queue: :gitlab_shell
+  include DedicatedSidekiqQueue
 
   def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
+    Gitlab::Metrics.add_event(:fork_repository,
+                              source_path: source_path,
+                              target_path: target_path)
+
     project = Project.find_by_id(project_id)
 
     unless project.present?
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 7d819fe78f83fc24699af26b4806b46b4dda19b0..c8a77e21c123fccbfcc5cc8b3b52be631b0249c4 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,8 +1,7 @@
 class RepositoryImportWorker
   include Sidekiq::Worker
   include Gitlab::ShellAdapter
-
-  sidekiq_options queue: :gitlab_shell
+  include DedicatedSidekiqQueue
 
   attr_accessor :project, :current_user
 
@@ -10,6 +9,12 @@ class RepositoryImportWorker
     @project = Project.find(project_id)
     @current_user = @project.creator
 
+    Gitlab::Metrics.add_event(:import_repository,
+                              import_url: @project.import_url,
+                              path: @project.path_with_namespace)
+
+    project.update_column(:import_error, nil)
+
     result = Projects::ImportService.new(project, current_user).execute
 
     if result[:status] == :error
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 9dd228a248377de6d5f01e7f7b3ff0dfec6e5ad3..703b025d76e86fd3d59a7d10175ccd01c3a2b87f 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,7 +1,6 @@
 class RequestsProfilesWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :default
+  include CronjobQueue
 
   def perform
     Gitlab::RequestProfiler.remove_all_profiles
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
index 6828013b3772d5fdcd9b26fdcc0c727dd70dff12..b70df5a1afaf061ad3077d3a161c7f0bf75d010f 100644
--- a/app/workers/stuck_ci_builds_worker.rb
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -1,5 +1,6 @@
 class StuckCiBuildsWorker
   include Sidekiq::Worker
+  include CronjobQueue
 
   BUILD_STUCK_TIMEOUT = 1.day
 
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
index a122c274763ad0ce41118de0499d11595379802f..baf2f12eeacaf3b2dd1cf25e7f631460b97a5ec7 100644
--- a/app/workers/system_hook_worker.rb
+++ b/app/workers/system_hook_worker.rb
@@ -1,7 +1,6 @@
 class SystemHookWorker
   include Sidekiq::Worker
-
-  sidekiq_options queue: :system_hook
+  include DedicatedSidekiqQueue
 
   def perform(hook_id, data, hook_name)
     SystemHook.find(hook_id).execute(data, hook_name)
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0531630d13a7b8de98387384201629e5a8840ea1
--- /dev/null
+++ b/app/workers/trending_projects_worker.rb
@@ -0,0 +1,10 @@
+class TrendingProjectsWorker
+  include Sidekiq::Worker
+  include CronjobQueue
+
+  def perform
+    Rails.logger.info('Refreshing trending projects')
+
+    TrendingProject.refresh!
+  end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..acc4d8581361168025ba6f4ed6d91c1ee3e69295
--- /dev/null
+++ b/app/workers/update_merge_requests_worker.rb
@@ -0,0 +1,17 @@
+class UpdateMergeRequestsWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+
+  def perform(project_id, user_id, oldrev, newrev, ref)
+    project = Project.find_by(id: project_id)
+    return unless project
+
+    user = User.find_by(id: user_id)
+    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/bin/background_jobs b/bin/background_jobs
index 25a578a1c491609b73832f0d9c3ee4c5a35e2081..f28e2f722dc77538eda2b7ce9a2ef3ccab1d201f 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -4,6 +4,7 @@ cd $(dirname $0)/..
 app_root=$(pwd)
 sidekiq_pidfile="$app_root/tmp/pids/sidekiq.pid"
 sidekiq_logfile="$app_root/log/sidekiq.log"
+sidekiq_config="$app_root/config/sidekiq_queues.yml"
 gitlab_user=$(ls -l config.ru | awk '{print $3}')
 
 warn()
@@ -37,7 +38,7 @@ start_no_deamonize()
 
 start_sidekiq()
 {
-  exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@"
+  exec bundle exec sidekiq -C "${sidekiq_config}" -e $RAILS_ENV -P $sidekiq_pidfile "$@"
 }
 
 load_ok()
diff --git a/bin/changelog b/bin/changelog
new file mode 100755
index 0000000000000000000000000000000000000000..b6586ebb6aae010adb486f269065b584a837d4ff
--- /dev/null
+++ b/bin/changelog
@@ -0,0 +1,170 @@
+#!/usr/bin/env ruby
+#
+# Generate a changelog entry file in the correct location.
+#
+# Automatically stages the file and amends the previous commit if the `--amend`
+# argument is used.
+
+require 'optparse'
+require 'yaml'
+
+Options = Struct.new(
+  :amend,
+  :author,
+  :dry_run,
+  :force,
+  :merge_request,
+  :title
+)
+
+class ChangelogOptionParser
+  def self.parse(argv)
+    options = Options.new
+
+    parser = OptionParser.new do |opts|
+      opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
+
+      # Note: We do not provide a shorthand for this in order to match the `git
+      # commit` interface
+      opts.on('--amend', 'Amend the previous commit') do |value|
+        options.amend = value
+      end
+
+      opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
+        options.force = value
+      end
+
+      opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
+        options.merge_request = value
+      end
+
+      opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
+        options.dry_run = value
+      end
+
+      opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
+        options.author = git_user_name if value
+      end
+
+      opts.on('-h', '--help', 'Print help message') do
+        $stdout.puts opts
+        exit
+      end
+    end
+
+    parser.parse!(argv)
+
+    # Title is everything that remains, but let's clean it up a bit
+    options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
+
+    options
+  end
+
+  def self.git_user_name
+    %x{git config user.name}.strip
+  end
+end
+
+class ChangelogEntry
+  attr_reader :options
+
+  def initialize(options)
+    @options = options
+
+    assert_feature_branch!
+    assert_new_file!
+    assert_title!
+
+    $stdout.puts "\e[32mcreate\e[0m #{file_path}"
+    $stdout.puts contents
+
+    unless options.dry_run
+      write
+      amend_commit if options.amend
+    end
+  end
+
+  def contents
+    YAML.dump(
+      'title'         => title,
+      'merge_request' => options.merge_request,
+      'author'        => options.author
+    )
+  end
+
+  def write
+    File.write(file_path, contents)
+  end
+
+  def amend_commit
+    %x{git add #{file_path}}
+    exec("git commit --amend")
+  end
+
+  private
+
+  def fail_with(message)
+    $stderr.puts "\e[31merror\e[0m #{message}"
+    exit 1
+  end
+
+  def assert_feature_branch!
+    return unless branch_name == 'master'
+
+    fail_with "Create a branch first!"
+  end
+
+  def assert_new_file!
+    return unless File.exist?(file_path)
+    return if options.force
+
+    fail_with "#{file_path} already exists! Use `--force` to overwrite."
+  end
+
+  def assert_title!
+    return if options.title.length > 0 || options.amend
+
+    fail_with "Provide a title for the changelog entry or use `--amend`" \
+      " to use the title from the previous commit."
+  end
+
+  def title
+    if options.title.empty?
+      last_commit_subject
+    else
+      options.title
+    end
+  end
+
+  def last_commit_subject
+    %x{git log --format="%s" -1}.strip
+  end
+
+  def file_path
+    File.join(
+      unreleased_path,
+      branch_name.gsub(/[^\w-]/, '-') << '.yml'
+    )
+  end
+
+  def unreleased_path
+    File.join('changelogs', 'unreleased').tap do |path|
+      path << '-ee' if ee?
+    end
+  end
+
+  def ee?
+    @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
+  end
+
+  def branch_name
+    @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '')
+  end
+end
+
+if $0 == __FILE__
+  options = ChangelogOptionParser.parse(ARGV)
+  ChangelogEntry.new(options)
+end
+
+# vim: ft=ruby
diff --git a/changelogs/archive.md b/changelogs/archive.md
new file mode 100644
index 0000000000000000000000000000000000000000..c68ab694d39bba375fac3ca7066186b7ba2c45d6
--- /dev/null
+++ b/changelogs/archive.md
@@ -0,0 +1,1810 @@
+## 7.14.3
+
+- No changes
+
+## 7.14.2
+
+- Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
+- Allow configuration of LDAP attributes GitLab will use for the new user account.
+
+## 7.14.1
+
+- Improve abuse reports management from admin area
+- Fix "Reload with full diff" URL button in compare branch view (Stan Hu)
+- Disabled DNS lookups for SSH in docker image (Rowan Wookey)
+- Only include base URL in OmniAuth full_host parameter (Stan Hu)
+- Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
+- Ability to enable SSL verification for Webhooks
+
+## 7.14.0
+
+- Fix bug where non-project members of the target project could set labels on new merge requests.
+- Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller)
+- Fix redirection after sign in when using auto_sign_in_with_provider
+- Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu)
+- Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu)
+- Provide more feedback what went wrong if HipChat service failed test (Stan Hu)
+- Fix bug where backslashes in inline diffs could be dropped (Stan Hu)
+- Disable turbolinks when linking to Bitbucket import status (Stan Hu)
+- Fix broken code import and display error messages if something went wrong with creating project (Stan Hu)
+- Fix corrupted binary files when using API files endpoint (Stan Hu)
+- Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu)
+- Show incompatible projects in Bitbucket import status (Stan Hu)
+- Fix coloring of diffs on MR Discussion-tab (Gert Goet)
+- Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu)
+- Fix errors deleting and creating branches with encoded slashes (Stan Hu)
+- Always add current user to autocomplete controller to support filter by "Me" (Stan Hu)
+- Fix multi-line syntax highlighting (Stan Hu)
+- Fix network graph when branch name has single quotes (Stan Hu)
+- Add "Confirm user" button in user admin page (Stan Hu)
+- Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu)
+- Add support for Unicode filenames in relative links (Hiroyuki Sato)
+- Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki)
+- Fix commit data retrieval when branch name has single quotes (Stan Hu)
+- Check that project was actually created rather than just validated in import:repos task (Stan Hu)
+- Fix full screen mode for snippet comments (Daniel Gerhardt)
+- Fix 404 error in files view after deleting the last file in a repository (Stan Hu)
+- Fix the "Reload with full diff" URL button (Stan Hu)
+- Fix label read access for unauthenticated users (Daniel Gerhardt)
+- Fix access to disabled features for unauthenticated users (Daniel Gerhardt)
+- Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu)
+- Fix file upload dialog for comment editing (Daniel Gerhardt)
+- Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu)
+- Return comments in created order in merge request API (Stan Hu)
+- Disable internal issue tracker controller if external tracker is used (Stan Hu)
+- Expire Rails cache entries after two weeks to prevent endless Redis growth
+- Add support for destroying project milestones (Stan Hu)
+- Allow custom backup archive permissions
+- Add project star and fork count, group avatar URL and user/group web URL attributes to API
+- Show who last edited a comment if it wasn't the original author
+- Send notification to all participants when MR is merged.
+- Add ability to manage user email addresses via the API.
+- Show buttons to add license, changelog and contribution guide if they're missing.
+- Tweak project page buttons.
+- Disabled autocapitalize and autocorrect on login field (Daryl Chan)
+- Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis)
+- Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller)
+- Remove redis-store TTL monkey patch
+- Add support for CI skipped status
+- Fetch code from forks to refs/merge-requests/:id/head when merge request created
+- Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg)
+- Add "Check out branch" button to the MR page.
+- Improve MR merge widget text and UI consistency.
+- Improve text in MR "How To Merge" modal.
+- Cache all events
+- Order commits by date when comparing branches
+- Fix bug causing error when the target branch of a symbolic ref was deleted
+- Include branch/tag name in archive file and directory name
+- Add dropzone upload progress
+- Add a label for merged branches on branches page (Florent Baldino)
+- Detect .mkd and .mkdn files as markdown (Ben Boeckel)
+- Fix: User search feature in admin area does not respect filters
+- Set max-width for README, issue and merge request description for easier read on big screens
+- Update Flowdock integration to support new Flowdock API (Boyan Tabakov)
+- Remove author from files view (Sven Strickroth)
+- Fix infinite loop when SAML was incorrectly configured.
+
+## 7.13.5
+
+- Satellites reverted
+
+## 7.13.4
+
+- Allow users to send abuse reports
+
+## 7.13.3
+
+- Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
+- Allow users to send abuse reports
+- Remove satellites
+- Link username to profile on Group Members page (Tom Webster)
+
+## 7.13.2
+
+- Fix randomly failed spec
+- Create project services on Project creation
+- Add admin_merge_request ability to Developer level and up
+- Fix Error 500 when browsing projects with no HEAD (Stan Hu)
+- Fix labels / assignee / milestone for the merge requests when issues are disabled
+- Show the first tab automatically on MergeRequests#new
+- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
+- Fix Gmail Actions
+
+## 7.13.1
+
+- Fix: Label modifications are not reflected in existing notes and in the issue list
+- Fix: Label not shown in the Issue list, although it's set through web interface
+- Fix: Group/project references are linked incorrectly
+- Improve documentation
+- Fix of migration: Check if session_expire_delay column exists before adding the column
+- Fix: ActionView::Template::Error
+- Fix: "Create Merge Request" isn't always shown in event for newly pushed branch
+- Fix bug causing "Remove source-branch" option not to work for merge requests from the same project.
+- Render Note field hints consistently for "new" and "edit" forms
+
+## 7.13.0
+
+- Remove repository graph log to fix slow cache updates after push event (Stan Hu)
+- Only enable HSTS header for HTTPS and port 443 (Stan Hu)
+- Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu)
+- Fix redirection to home page URL for unauthorized users (Daniel Gerhardt)
+- Add branch switching support for graphs (Daniel Gerhardt)
+- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
+- Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
+- Add support for unlocking users in admin settings (Stan Hu)
+- Add Irker service configuration options (Stan Hu)
+- Fix order of issues imported from GitHub (Hiroyuki Sato)
+- Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
+- Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
+- Add `two_factor_enabled` field to admin user API (Stan Hu)
+- Fix invalid timestamps in RSS feeds (Rowan Wookey)
+- Fix downloading of patches on public merge requests when user logged out (Stan Hu)
+- Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu)
+- Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu)
+- Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
+- Support commenting on diffs in side-by-side mode (Stan Hu)
+- Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
+- Return 40x error codes if branch could not be deleted in UI (Stan Hu)
+- Remove project visibility icons from dashboard projects list
+- Rename "Design" profile settings page to "Preferences".
+- Allow users to customize their default Dashboard page.
+- Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
+- Admin can edit and remove user identities
+- Convert CRLF newlines to LF when committing using the web editor.
+- API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged.
+- Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled.
+- Show a user's Two-factor Authentication status in the administration area.
+- Explicit error when commit not found in the CI
+- Improve performance for issue and merge request pages
+- Users with guest access level can not set assignee, labels or milestones for issue and merge request
+- Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
+- Better performance for pages with events list, issues list and commits list
+- Faster automerge check and merge itself when source and target branches are in same repository
+- Correctly show anonymous authorized applications under Profile > Applications.
+- Query Optimization in MySQL.
+- Allow users to be blocked and unblocked via the API
+- Use native Postgres database cleaning during backup restore
+- Redesign project page. Show README as default instead of activity. Move project activity to separate page
+- Make left menu more hierarchical and less contextual by adding back item at top
+- A fork can’t have a visibility level that is greater than the original project.
+- Faster code search in repository and wiki. Fixes search page timeout for big repositories
+- Allow administrators to disable 2FA for a specific user
+- Add error message for SSH key linebreaks
+- Store commits count in database (will populate with valid values only after first push)
+- Rebuild cache after push to repository in background job
+- Fix transferring of project to another group using the API.
+
+## 7.12.2
+
+- Correctly show anonymous authorized applications under Profile > Applications.
+- Faster automerge check and merge itself when source and target branches are in same repository
+- Audit log for user authentication
+- Allow custom label to be set for authentication providers.
+
+## 7.12.1
+
+- Fix error when deleting a user who has projects (Stan Hu)
+- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
+- Add SAML to list of social_provider (Matt Firtion)
+- Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets)
+- Fix closed merge request scope at milestone page (Dmitriy Zaporozhets)
+- Revert merge request states renaming
+- Fix hooks for web based events with external issue references (Daniel Gerhardt)
+- Improve performance for issue and merge request pages
+- Compress database dumps to reduce backup size
+
+## 7.12.0
+
+- Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
+- Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
+- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
+- Update oauth button logos for Twitter and Google to recommended assets
+- Update browser gem to version 0.8.0 for IE11 support (Stan Hu)
+- Fix timeout when rendering file with thousands of lines.
+- Add "Remember me" checkbox to LDAP signin form.
+- Add session expiration delay configuration through UI application settings
+- Don't notify users mentioned in code blocks or blockquotes.
+- Omit link to generate labels if user does not have access to create them (Stan Hu)
+- Show warning when a comment will add 10 or more people to the discussion.
+- Disable changing of the source branch in merge request update API (Stan Hu)
+- Shorten merge request WIP text.
+- Add option to disallow users from registering any application to use GitLab as an OAuth provider
+- Support editing target branch of merge request (Stan Hu)
+- Refactor permission checks with issues and merge requests project settings (Stan Hu)
+- Fix Markdown preview not working in Edit Milestone page (Stan Hu)
+- Fix Zen Mode not closing with ESC key (Stan Hu)
+- Allow HipChat API version to be blank and default to v2 (Stan Hu)
+- Add file attachment support in Milestone description (Stan Hu)
+- Fix milestone "Browse Issues" button.
+- Set milestone on new issue when creating issue from index with milestone filter active.
+- Make namespace API available to all users (Stan Hu)
+- Add webhook support for note events (Stan Hu)
+- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
+- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
+- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
+- Fix git blame syntax highlighting when different commits break up lines (Stan Hu)
+- Add "Resend confirmation e-mail" link in profile settings (Stan Hu)
+- Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka)
+- Disabled expansion of top/bottom blobs for new file diffs
+- Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka)
+- Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka)
+- Use the user list from the target project in a merge request (Stan Hu)
+- Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen)
+- Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen)
+- Fix new/empty milestones showing 100% completion value (Jonah Bishop)
+- Add a note when an Issue or Merge Request's title changes
+- Consistently refer to MRs as either Merged or Closed.
+- Add Merged tab to MR lists.
+- Prefix EmailsOnPush email subject with `[Git]`.
+- Group project contributions by both name and email.
+- Clarify navigation labels for Project Settings and Group Settings.
+- Move user avatar and logout button to sidebar
+- You can not remove user if he/she is an only owner of group
+- User should be able to leave group. If not - show him proper message
+- User has ability to leave project
+- Add SAML support as an omniauth provider
+- Allow to configure a URL to show after sign out
+- Add an option to automatically sign-in with an Omniauth provider
+- GitLab CI service sends .gitlab-ci.yml in each push call
+- When remove project - move repository and schedule it removal
+- Improve group removing logic
+- Trigger create-hooks on backup restore task
+- Add option to automatically link omniauth and LDAP identities
+- Allow special character in users bio. I.e.: I <3 GitLab
+
+## 7.11.4
+
+- Fix missing bullets when creating lists
+- Set rel="nofollow" on external links
+
+## 7.11.3
+
+- no changes
+- Fix upgrader script (Martins Polakovs)
+
+## 7.11.2
+
+- no changes
+
+## 7.11.1
+
+- no changes
+
+## 7.11.0
+
+- Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger)
+- Get editing comments to work in Chrome 43 again.
+- Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu)
+- Don't show duplicate deploy keys
+- Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger)
+- Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
+- Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu)
+- Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
+- Add application setting to restrict user signups to e-mail domains (Stan Hu)
+- Don't allow a merge request to be merged when its title starts with "WIP".
+- Add a page title to every page.
+- Allow primary email to be set to an email that you've already added.
+- Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
+- Ignore invalid lines in .gitmodules
+- Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
+- Redirect to sign in page after signing out.
+- Fix "Hello @username." references not working by no longer allowing usernames to end in period.
+- Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu)
+- Improve project page UI
+- Fix broken file browsing with relative submodule in personal projects (Stan Hu)
+- Add "Reply quoting selected text" shortcut key (`r`)
+- Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
+- Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
+- When use change branches link at MR form - save source branch selection instead of target one
+- Improve handling of large diffs
+- Added GitLab Event header for project hooks
+- Add Two-factor authentication (2FA) for GitLab logins
+- Show Atom feed buttons everywhere where applicable.
+- Add project activity atom feed.
+- Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
+- Explain how to get a new password reset token in welcome emails
+- Include commit comments in MR from a forked project.
+- Group milestones by title in the dashboard and all other issue views.
+- Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
+- Add default project and snippet visibility settings to the admin web UI.
+- Show incompatible projects in Google Code import status (Stan Hu)
+- Fix bug where commit data would not appear in some subdirectories (Stan Hu)
+- Task lists are now usable in comments, and will show up in Markdown previews.
+- Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu)
+- Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
+- Protect OmniAuth request phase against CSRF.
+- Don't send notifications to mentioned users that don't have access to the project in question.
+- Add search issues/MR by number
+- Change plots to bar graphs in commit statistics screen
+- Move snippets UI to fluid layout
+- Improve UI for sidebar. Increase separation between navigation and content
+- Improve new project command options (Ben Bodenmiller)
+- Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
+- Prevent sending empty messages to HipChat (Chulki Lee)
+- Improve UI for mobile phones on dashboard and project pages
+- Add room notification and message color option for HipChat
+- Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
+- Add footnotes support to Markdown (Guillaume Delbergue)
+- Add current_sign_in_at to UserFull REST api.
+- Make Sidekiq MemoryKiller shutdown signal configurable
+- Add "Create Merge Request" buttons to commits and branches pages and push event.
+- Show user roles by comments.
+- Fix automatic blocking of auto-created users from Active Directory.
+- Call merge request webhook for each new commits (Arthur Gautier)
+- Use SIGKILL by default in Sidekiq::MemoryKiller
+- Fix mentioning of private groups.
+- Add style for <kbd> element in markdown
+- Spin spinner icon next to "Checking for CI status..." on MR page.
+- Fix reference links in dashboard activity and ATOM feeds.
+- Ensure that the first added admin performs repository imports
+
+## 7.10.4
+
+- Fix migrations broken in 7.10.2
+- Make tags for GitLab installations running on MySQL case sensitive
+- Get Gitorious importer to work again.
+- Fix adding new group members from admin area
+- Fix DB error when trying to tag a repository (Stan Hu)
+- Fix Error 500 when searching Wiki pages (Stan Hu)
+- Unescape branch names in compare commit (Stan Hu)
+- Order commit comments chronologically in API.
+
+## 7.10.2
+
+- Fix CI links on MR page
+
+## 7.10.0
+
+- Ignore submodules that are defined in .gitmodules but are checked in as directories.
+- Allow projects to be imported from Google Code.
+- Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger)
+- Allow users to be invited by email to join a group or project.
+- Don't crash when project repository doesn't exist.
+- Add config var to block auto-created LDAP users.
+- Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
+- Set EmailsOnPush reply-to address to committer email when enabled.
+- Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
+- Fix persistent XSS vulnerability around profile website URLs.
+- Fix project import URL regex to prevent arbitary local repos from being imported.
+- Fix directory traversal vulnerability around uploads routes.
+- Fix directory traversal vulnerability around help pages.
+- Don't leak existence of project via search autocomplete.
+- Don't leak existence of group or project via search.
+- Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu)
+- Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu)
+- Add a rake task to check repository integrity with `git fsck`
+- Add ability to configure Reply-To address in gitlab.yml (Stan Hu)
+- Move current user to the top of the list in assignee/author filters (Stan Hu)
+- Fix broken side-by-side diff view on merge request page (Stan Hu)
+- Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
+- Allow HTML tags in Markdown input
+- Fix code unfold not working on Compare commits page (Stan Hu)
+- Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
+- Fix "Import projects from" button to show the correct instructions (Stan Hu)
+- Fix dots in Wiki slugs causing errors (Stan Hu)
+- Make maximum attachment size configurable via Application Settings (Stan Hu)
+- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
+- Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
+- Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
+- Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
+- enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
+- Fix a link in the patch update guide
+- Add a service to support external wikis (Hannes Rosenögger)
+- Omit the "email patches" link and fix plain diff view for merge commits
+- List new commits for newly pushed branch in activity view.
+- Add sidetiq gem dependency to match EE
+- Add changelog, license and contribution guide links to project tab bar.
+- Improve diff UI
+- Fix alignment of navbar toggle button (Cody Mize)
+- Fix checkbox rendering for nested task lists
+- Identical look of selectboxes in UI
+- Upgrade the gitlab_git gem to version 7.1.3
+- Move "Import existing repository by URL" option to button.
+- Improve error message when save profile has error.
+- Passing the name of pushed ref to CI service (requires GitLab CI 7.9+)
+- Add location field to user profile
+- Fix print view for markdown files and wiki pages
+- Fix errors when deleting old backups
+- Improve GitLab performance when working with git repositories
+- Add tag message and last commit to tag hook (Kamil Trzciński)
+- Restrict permissions on backup files
+- Improve oauth accounts UI in profile page
+- Add ability to unlink connected accounts
+- Replace commits calendar with faster contribution calendar that includes issues and merge requests
+- Add inifinite scroll to user page activity
+- Don't include system notes in issue/MR comment count.
+- Don't mark merge request as updated when merge status relative to target branch changes.
+- Link note avatar to user.
+- Make Git-over-SSH errors more descriptive.
+- Fix EmailsOnPush.
+- Refactor issue filtering
+- AJAX selectbox for issue assignee and author filters
+- Fix issue with missing options in issue filtering dropdown if selected one
+- Prevent holding Control-Enter or Command-Enter from posting comment multiple times.
+- Prevent note form from being cleared when submitting failed.
+- Improve file icons rendering on tree (Sullivan Sénéchal)
+- API: Add pagination to project events
+- Get issue links in notification mail to work again.
+- Don't show commit comment button when user is not signed in.
+- Fix admin user projects lists.
+- Don't leak private group existence by redirecting from namespace controller to group controller.
+- Ability to skip some items from backup (database, respositories or uploads)
+- Archive repositories in background worker.
+- Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace.
+- Project labels are now available over the API under the "tag_list" field (Cristian Medina)
+- Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz)
+- Fix and improve help rendering (Sullivan Sénéchal)
+- Fix final line in EmailsOnPush email diff being rendered as error.
+- Prevent duplicate Buildkite service creation.
+- Fix git over ssh errors 'fatal: protocol error: bad line length character'
+- Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled
+- Bust group page project list cache when namespace name or path changes.
+- Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded
+- Allow user to choose a public email to show on public profile
+- Remove truncation from issue titles on milestone page (Jason Blanchard)
+- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
+- Fix merge request comments on files with multiple commits
+- Fix Resource Owner Password Authentication Flow
+- Add icons to Add dropdown items.
+- Allow admin to create public deploy keys that are accessible to any project.
+- Warn when gitlab-shell version doesn't match requirement.
+- Skip email confirmation when set by admin or via LDAP.
+- Only allow users to reference groups, projects, issues, MRs, commits they have access to.
+
+## 7.9.4
+
+- Security: Fix project import URL regex to prevent arbitary local repos from being imported
+- Fixed issue where only 25 commits would load in file listings
+- Fix LDAP identities  after config update
+
+## 7.9.3
+
+- Contains no changes
+
+## 7.9.2
+
+- Contains no changes
+
+## 7.9.1
+
+- Include missing events and fix save functionality in admin service template settings form (Stan Hu)
+- Fix "Import projects from" button to show the correct instructions (Stan Hu)
+- Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
+- Fix for LDAP with commas in DN
+- Fix missing events and in admin Slack service template settings form (Stan Hu)
+- Don't show commit comment button when user is not signed in.
+- Downgrade gemnasium-gitlab-service gem
+
+## 7.9.0
+
+- Add HipChat integration documentation (Stan Hu)
+- Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
+- Fix broken email images (Hannes Rosenögger)
+- Automatically config git if user forgot, where possible (Zeger-Jan van de Weg)
+- Fix mass SQL statements on initial push (Hannes Rosenögger)
+- Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu)
+- Add comment notification events to HipChat and Slack services (Stan Hu)
+- Add issue and merge request events to HipChat and Slack services (Stan Hu)
+- Fix merge request URL passed to Webhooks. (Stan Hu)
+- Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
+- Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
+- Move labels/milestones tabs to sidebar
+- Upgrade Rails gem to version 4.1.9.
+- Improve error messages for file edit failures
+- Improve UI for commits, issues and merge request lists
+- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+- Allow admins to override restricted project visibility settings.
+- Move restricted visibility settings from gitlab.yml into the web UI.
+- Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
+- Save web edit in new branch
+- Fix ordering of imported but unchanged projects (Marco Wessel)
+- Mobile UI improvements: make aside content expandable
+- Expose avatar_url in projects API
+- Fix checkbox alignment on the application settings page.
+- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
+- Fix mass-unassignment of issues (Robert Speicher)
+- Fix hidden diff comments in merge request discussion view
+- Allow user confirmation to be skipped for new users via API
+- Add a service to send updates to an Irker gateway (Romain Coltel)
+- Add brakeman (security scanner for Ruby on Rails)
+- Slack username and channel options
+- Add grouped milestones from all projects to dashboard.
+- Webhook sends pusher email as well as commiter
+- Add Bitbucket omniauth provider.
+- Add Bitbucket importer.
+- Support referencing issues to a project whose name starts with a digit
+- Condense commits already in target branch when updating merge request source branch.
+- Send notifications and leave system comments when bulk updating issues.
+- Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
+- Move groups page from profile to dashboard
+- Starred projects page at dashboard
+- Blocking user does not remove him/her from project/groups but show blocked label
+- Change subject of EmailsOnPush emails to include namespace, project and branch.
+- Change subject of EmailsOnPush emails to include first commit message when multiple were pushed.
+- Remove confusing footer from EmailsOnPush mail body.
+- Add list of changed files to EmailsOnPush emails.
+- Add option to send EmailsOnPush emails from committer email if domain matches.
+- Add option to disable code diffs in EmailOnPush emails.
+- Wrap commit message in EmailsOnPush email.
+- Send EmailsOnPush emails when deleting commits using force push.
+- Fix EmailsOnPush email comparison link to include first commit.
+- Fix highliht of selected lines in file
+- Reject access to group/project avatar if the user doesn't have access.
+- Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update)
+- Add GitLab active users count to rake gitlab:check
+- Starred projects page at dashboard
+- Make email display name configurable
+- Improve json validation in hook data
+- Use Emoji One
+- Updated emoji help documentation to properly reference EmojiOne.
+- Fix missing GitHub organisation repositories on import page.
+- Added blue theme
+- Remove annoying notice messages when create/update merge request
+- Allow smb:// links in Markdown text.
+- Filter merge request by title or description at Merge Requests page
+- Block user if he/she was blocked in Active Directory
+- Fix import pages not working after first load.
+- Use custom LDAP label in LDAP signin form.
+- Execute hooks and services when branch or tag is created or deleted through web interface.
+- Block and unblock user if he/she was blocked/unblocked in Active Directory
+- Raise recommended number of unicorn workers from 2 to 3
+- Use same layout and interactivity for project members as group members.
+- Prevent gitlab-shell character encoding issues by receiving its changes as raw data.
+- Ability to unsubscribe/subscribe to issue or merge request
+- Delete deploy key when last connection to a project is destroyed.
+- Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther)
+- Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup)
+- Add canceled status for CI
+- Send EmailsOnPush email when branch or tag is created or deleted.
+- Faster merge request processing for large repository
+- Prevent doubling AJAX request with each commit visit via Turbolink
+- Prevent unnecessary doubling of js events on import pages and user calendar
+
+## 7.8.4
+
+- Fix issue_tracker_id substitution in custom issue trackers
+- Fix path and name duplication in namespaces
+
+## 7.8.3
+
+- Bump version of gitlab_git fixing annotated tags without message
+
+## 7.8.2
+
+- Fix service migration issue when upgrading from versions prior to 7.3
+- Fix setting of the default use project limit via admin UI
+- Fix showing of already imported projects for GitLab and Gitorious importers
+- Fix response of push to repository to return "Not found" if user doesn't have access
+- Fix check if user is allowed to view the file attachment
+- Fix import check for case sensetive namespaces
+- Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time.
+- Properly handle autosave local storage exceptions.
+- Escape wildcards when searching LDAP by username.
+
+## 7.8.1
+
+- Fix run of custom post receive hooks
+- Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3
+- Fix the warning for LDAP users about need to set password
+- Fix avatars which were not shown for non logged in users
+- Fix urls for the issues when relative url was enabled
+
+## 7.8.0
+
+- Fix access control and protection against XSS for note attachments and other uploads.
+- Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
+- Make project search case insensitive (Hannes Rosenögger)
+- Include issue/mr participants in list of recipients for reassign/close/reopen emails
+- Expose description in groups API
+- Better UI for project services page
+- Cleaner UI for web editor
+- Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger)
+- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
+- View note image attachments in new tab when clicked instead of downloading them
+- Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
+- Fix overflow at sidebar when have several items
+- Add notes for label changes in issue and merge requests
+- Show tags in commit view (Hannes Rosenögger)
+- Only count a user's vote once on a merge request or issue (Michael Clarke)
+- Increase font size when browse source files and diffs
+- Service Templates now let you set default values for all services
+- Create new file in empty repository using GitLab UI
+- Ability to clone project using oauth2 token
+- Upgrade Sidekiq gem to version 3.3.0
+- Stop git zombie creation during force push check
+- Show success/error messages for test setting button in services
+- Added Rubocop for code style checks
+- Fix commits pagination
+- Async load a branch information at the commit page
+- Disable blacklist validation for project names
+- Allow configuring protection of the default branch upon first push (Marco Wessel)
+- Add gitlab.com importer
+- Add an ability to login with gitlab.com
+- Add a commit calendar to the user profile (Hannes Rosenögger)
+- Submit comment on command-enter
+- Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
+- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
+- Fix long broadcast message cut-off on left sidebar (Visay Keo)
+- Add Project Avatars (Steven Thonus and Hannes Rosenögger)
+- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
+- Edit group members via API
+- Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks)
+- Add action property to merge request hook (Julien Bianchi)
+- Remove duplicates from group milestone participants list.
+- Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger)
+- API: Access groups with their path (Julien Bianchi)
+- Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard)
+- Allow notification email to be set separately from primary email.
+- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
+- Don't have Markdown preview fail for long comments/wiki pages.
+- When test webhook - show error message instead of 500 error page if connection to hook url was reset
+- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
+- Added persistent collapse button for left side nav bar (Jason Blanchard)
+- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
+- Don't allow page to be scaled on mobile.
+- Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up.
+- Show assignees in merge request index page (Kelvin Mutuma)
+- Link head panel titles to relevant root page.
+- Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S).
+- Show users button to share their newly created public or internal projects on twitter
+- Add quick help links to the GitLab pricing and feature comparison pages.
+- Fix duplicate authorized applications in user profile and incorrect application client count in admin area.
+- Make sure Markdown previews always use the same styling as the eventual destination.
+- Remove deprecated Group#owner_id from API
+- Show projects user contributed to on user page. Show stars near project on user page.
+- Improve database performance for GitLab
+- Add Asana service (Jeremy Benoist)
+- Improve project webhooks with extra data
+
+## 7.7.2
+
+- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
+- Fix issue when LDAP user can't login with existing GitLab account
+
+## 7.7.1
+
+- Improve mention autocomplete performance
+- Show setup instructions for GitHub import if disabled
+- Allow use http for OAuth applications
+
+## 7.7.0
+
+- Import from GitHub.com feature
+- Add Jetbrains Teamcity CI service (Jason Lippert)
+- Mention notification level
+- Markdown preview in wiki (Yuriy Glukhov)
+- Raise group avatar filesize limit to 200kb
+- OAuth applications feature
+- Show user SSH keys in admin area
+- Developer can push to protected branches option
+- Set project path instead of project name in create form
+- Block Git HTTP access after 10 failed authentication attempts
+- Updates to the messages returned by API (sponsored by O'Reilly Media)
+- New UI layout with side navigation
+- Add alert message in case of outdated browser (IE < 10)
+- Added API support for sorting projects
+- Update gitlab_git to version 7.0.0.rc14
+- Add API project search filter option for authorized projects
+- Fix File blame not respecting branch selection
+- Change some of application settings on fly in admin area UI
+- Redesign signin/signup pages
+- Close standard input in Gitlab::Popen.popen
+- Trigger GitLab CI when push tags
+- When accept merge request - do merge using sidaekiq job
+- Enable web signups by default
+- Fixes for diff comments: drag-n-drop images, selecting images
+- Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
+- Remove password strength indicator
+
+## 7.6.0
+
+- Fork repository to groups
+- New rugged version
+- Add CRON=1 backup setting for quiet backups
+- Fix failing wiki restore
+- Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable)
+- Monokai highlighting style now more faithful to original design (Mark Riedesel)
+- Create project with repository in synchrony
+- Added ability to create empty repo or import existing one if project does not have repository
+- Reactivate highlight.js language autodetection
+- Mobile UI improvements
+- Change maximum avatar file size from 100KB to 200KB
+- Strict validation for snippet file names
+- Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada)
+- In the docker directory is a container template based on the Omnibus packages.
+- Update Sidekiq to version 2.17.8
+- Add author filter to project issues and merge requests pages
+- Atom feed for user activity
+- Support multiple omniauth providers for the same user
+- Rendering cross reference in issue title and tooltip for merge request
+- Show username in comments
+- Possibility to create Milestones or Labels when Issues are disabled
+- Fix bug with showing gpg signature in tag
+
+## 7.5.3
+
+- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
+
+## 7.5.2
+
+- Don't log Sidekiq arguments by default
+- Fix restore of wiki repositories from backups
+
+## 7.5.1
+
+- Add missing timestamps to 'members' table
+
+## 7.5.0
+
+- API: Add support for Hipchat (Kevin Houdebert)
+- Add time zone configuration in gitlab.yml (Sullivan Senechal)
+- Fix LDAP authentication for Git HTTP access
+- Run 'GC.start' after every EmailsOnPushWorker job
+- Fix LDAP config lookup for provider 'ldap'
+- Drop all sequences during Postgres database restore
+- Project title links to project homepage (Ben Bodenmiller)
+- Add Atlassian Bamboo CI service (Drew Blessing)
+- Mentioned @user will receive email even if he is not participating in issue or commit
+- Session API: Use case-insensitive authentication like in UI (Andrey Krivko)
+- Tie up loose ends with annotated tags: API & UI (Sean Edge)
+- Return valid json for deleting branch via API (sponsored by O'Reilly Media)
+- Expose username in project events API (sponsored by O'Reilly Media)
+- Adds comments to commits in the API
+- Performance improvements
+- Fix post-receive issue for projects with deleted forks
+- New gitlab-shell version with custom hooks support
+- Improve code
+- GitLab CI 5.2+ support (does not support older versions)
+- Fixed bug when you can not push commits starting with 000000 to protected branches
+- Added a password strength indicator
+- Change project name and path in one form
+- Display renamed files in diff views (Vinnie Okada)
+- Fix raw view for public snippets
+- Use secret token with GitLab internal API.
+- Add missing timestamps to 'members' table
+
+## 7.4.5
+
+- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
+
+## 7.4.4
+
+- No changes
+
+## 7.4.3
+
+- Fix raw snippets view
+- Fix security issue for member api
+- Fix buildbox integration
+
+## 7.4.2
+
+- Fix internal snippet exposing for unauthenticated users
+
+## 7.4.1
+
+- Fix LDAP authentication for Git HTTP access
+- Fix LDAP config lookup for provider 'ldap'
+- Fix public snippets
+- Fix 500 error on projects with nested submodules
+
+## 7.4.0
+
+- Refactored membership logic
+- Improve error reporting on users API (Julien Bianchi)
+- Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
+- Default branch is protected by default
+- Increase unicorn timeout to 60 seconds
+- Sort search autocomplete projects by stars count so most popular go first
+- Add README to tab on project show page
+- Do not delete tmp/repositories itself during clean-up, only its contents
+- Support for backup uploads to remote storage
+- Prevent notes polling when there are not notes
+- Internal ForkService: Prepare support for fork to a given namespace
+- API: Add support for forking a project via the API (Bernhard Kaindl)
+- API: filter project issues by milestone (Julien Bianchi)
+- Fail harder in the backup script
+- Changes to Slack service structure, only webhook url needed
+- Zen mode for wiki and milestones (Robert Schilling)
+- Move Emoji parsing to html-pipeline-gitlab (Robert Schilling)
+- Font Awesome 4.2 integration (Sullivan Senechal)
+- Add Pushover service integration (Sullivan Senechal)
+- Add select field type for services options (Sullivan Senechal)
+- Add cross-project references to the Markdown parser (Vinnie Okada)
+- Add task lists to issue and merge request descriptions (Vinnie Okada)
+- Snippets can be public, internal or private
+- Improve danger zone: ask project path to confirm data-loss action
+- Raise exception on forgery
+- Show build coverage in Merge Requests (requires GitLab CI v5.1)
+- New milestone and label links on issue edit form
+- Improved repository graphs
+- Improve event note display in dashboard and project activity views (Vinnie Okada)
+- Add users sorting to admin area
+- UI improvements
+- Fix ambiguous sha problem with mentioned commit
+- Fixed bug with apostrophe when at mentioning users
+- Add active directory ldap option
+- Developers can push to wiki repo. Protected branches does not affect wiki repo any more
+- Faster rev list
+- Fix branch removal
+
+## 7.3.2
+
+- Fix creating new file via web editor
+- Use gitlab-shell v2.0.1
+
+## 7.3.1
+
+- Fix ref parsing in Gitlab::GitAccess
+- Fix error 500 when viewing diff on a file with changed permissions
+- Fix adding comments to MR when source branch is master
+- Fix error 500 when searching description contains relative link
+
+## 7.3.0
+
+- Always set the 'origin' remote in satellite actions
+- Write authorized_keys in tmp/ during tests
+- Use sockets to connect to Redis
+- Add dormant New Relic gem (can be enabled via environment variables)
+- Expire Rack sessions after 1 week
+- Cleaner signin/signup pages
+- Improved comments UI
+- Better search with filtering, pagination etc
+- Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov)
+- Prevent project stars duplication when fork project
+- Use the default Unicorn socket backlog value of 1024
+- Support Unix domain sockets for Redis
+- Store session Redis keys in 'session:gitlab:' namespace
+- Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
+- Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy)
+- Keyboard shortcuts for productivity (Robert Schilling)
+- API: filter issues by state (Julien Bianchi)
+- API: filter issues by labels (Julien Bianchi)
+- Add system hook for ssh key changes
+- Add blob permalink link (Ciro Santilli)
+- Create annotated tags through UI and API (Sean Edge)
+- Snippets search (Charles Bushong)
+- Comment new push to existing MR
+- Add 'ci' to the blacklist of forbidden names
+- Improve text filtering on issues page
+- Comment & Close button
+- Process git push --all much faster
+- Don't allow edit of system notes
+- Project wiki search (Ralf Seidler)
+- Enabled Shibboleth authentication support (Matus Banas)
+- Zen mode (fullscreen) for issues/MR/notes (Robert Schilling)
+- Add ability to configure webhook timeout via gitlab.yml (Wes Gurney)
+- Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media)
+- Add Redis socket support to 'rake gitlab:shell:install'
+
+## 7.2.1
+
+- Delete orphaned labels during label migration (James Brooks)
+- Security: prevent XSS with stricter MIME types for raw repo files
+
+## 7.2.0
+
+- Explore page
+- Add project stars (Ciro Santilli)
+- Log Sidekiq arguments
+- Better labels: colors, ability to rename and remove
+- Improve the way merge request collects diffs
+- Improve compare page for large diffs
+- Expose the full commit message via API
+- Fix 500 error on repository rename
+- Fix bug when MR download patch return invalid diff
+- Test gitlab-shell integration
+- Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported
+- API for labels (Robert Schilling)
+- API: ability to set an import url when creating project for specific user
+
+## 7.1.1
+
+- Fix cpu usage issue in Firefox
+- Fix redirect loop when changing password by new user
+- Fix 500 error on new merge request page
+
+## 7.1.0
+
+- Remove observers
+- Improve MR discussions
+- Filter by description on Issues#index page
+- Fix bug with namespace select when create new project page
+- Show README link after description for non-master members
+- Add @all mention for comments
+- Dont show reply button if user is not signed in
+- Expose more information for issues with webhook
+- Add a mention of the merge request into the default merge request commit message
+- Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc
+- Fix concurrency issue in repository download
+- Dont allow repository name start with ?
+- Improve email threading (Pierre de La Morinerie)
+- Cleaner help page
+- Group milestones
+- Improved email notifications
+- Contributors API (sponsored by Mobbr)
+- Fix LDAP TLS authentication (Boris HUISGEN)
+- Show VERSION information on project sidebar
+- Improve branch removal logic when accept MR
+- Fix bug where comment form is spawned inside the Reply button
+- Remove Dir.chdir from Satellite#lock for thread-safety
+- Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs!
+- Show error message in case of timeout in satellite when create MR
+- Show first 100 files for huge diff instead of hiding all
+- Change default admin email from admin@local.host to admin@example.com
+
+## 7.0.0
+
+- The CPU no longer overheats when you hold down the spacebar
+- Improve edit file UI
+- Add ability to upload group avatar when create
+- Protected branch cannot be removed
+- Developers can remove normal branches with UI
+- Remove branch via API (sponsored by O'Reilly Media)
+- Move protected branches page to Project settings area
+- Redirect to Files view when create new branch via UI
+- Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso)
+- Refactor the markdown relative links processing
+- Make it easier to implement other CI services for GitLab
+- Group masters can create projects in group
+- Deprecate ruby 1.9.3 support
+- Only masters can rewrite/remove git tags
+- Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible
+- UI improvements
+- Case-insensetive search for issues
+- Update to rails 4.1
+- Improve performance of application for projects and groups with a lot of members
+- Formally support Ruby 2.1
+- Include Nginx gitlab-ssl config
+- Add manual language detection for highlight.js
+- Added example.com/:username routing
+- Show notice if your profile is public
+- UI improvements for mobile devices
+- Improve diff rendering performance
+- Drag-n-drop for issues and merge requests between states at milestone page
+- Fix '0 commits' message for huge repositories on project home page
+- Prevent 500 error page when visit commit page from large repo
+- Add notice about huge push over http to unicorn config
+- File action in satellites uses default 30 seconds timeout instead of old 10 seconds one
+- Overall performance improvements
+- Skip init script check on omnibus-gitlab
+- Be more selective when killing stray Sidekiqs
+- Check LDAP user filter during sign-in
+- Remove wall feature (no data loss - you can take it from database)
+- Dont expose user emails via API unless you are admin
+- Detect issues closed by Merge Request description
+- Better email subject lines from email on push service (Alex Elman)
+- Enable identicon for gravatar be default
+
+## 6.9.2
+
+- Revert the commit that broke the LDAP user filter
+
+## 6.9.1
+
+- Fix scroll to highlighted line
+- Fix the pagination on load for commits page
+
+## 6.9.0
+
+- Store Rails cache data in the Redis `cache:gitlab` namespace
+- Adjust MySQL limits for existing installations
+- Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed)
+- Markdown preview or diff during editing via web editor (Evgeniy Sokovikov)
+- Give the Rails cache its own Redis namespace
+- Add ability to set different ssh host, if different from http/https
+- Fix syntax highlighting for code comments blocks
+- Improve comments loading logic
+- Stop refreshing comments when the tab is hidden
+- Improve issue and merge request mobile UI (Drew Blessing)
+- Document how to convert a backup to PostgreSQL
+- Fix locale bug in backup manager
+- Fix can not automerge when MR description is too long
+- Fix wiki backup skip bug
+- Two Step MR creation process
+- Remove unwanted files from satellite working directory with git clean -fdx
+- Accept merge request via API (sponsored by O'Reilly Media)
+- Add more access checks during API calls
+- Block SSH access for 'disabled' Active Directory users
+- Labels for merge requests (Drew Blessing)
+- Threaded emails by setting a Message-ID (Philip Blatter)
+
+## 6.8.0
+
+- Ability to at mention users that are participating in issue and merge req. discussion
+- Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu)
+- Make user search case-insensitive (Christopher Arnold)
+- Remove omniauth-ldap nickname bug workaround
+- Drop all tables before restoring a Postgres backup
+- Make the repository downloads path configurable
+- Create branches via API (sponsored by O'Reilly Media)
+- Changed permission of gitlab-satellites directory not to be world accessible
+- Protected branch does not allow force push
+- Fix popen bug in `rake gitlab:satellites:create`
+- Disable connection reaping for MySQL
+- Allow oauth signup without email for twitter and github
+- Fix faulty namespace names that caused 500 on user creation
+- Option to disable standard login
+- Clean old created archives from repository downloads directory
+- Fix download link for huge MR diffs
+- Expose event and mergerequest timestamps in API
+- Fix emails on push service when only one commit is pushed
+
+## 6.7.3
+
+- Fix the merge notification email not being sent (Pierre de La Morinerie)
+- Drop all tables before restoring a Postgres backup
+- Remove yanked modernizr gem
+
+## 6.7.2
+
+- Fix upgrader script
+
+## 6.7.1
+
+- Fix GitLab CI integration
+
+## 6.7.0
+
+- Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations
+- Add support for Gemnasium as a Project Service (Olivier Gonzalez)
+- Add edit file button to MergeRequest diff
+- Public groups (Jason Hollingsworth)
+- Cleaner headers in Notification Emails (Pierre de La Morinerie)
+- Blob and tree gfm links to anchors work
+- Piwik Integration (Sebastian Winkler)
+- Show contribution guide link for new issue form (Jeroen van Baarsen)
+- Fix CI status for merge requests from fork
+- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
+- New page load indicator that includes a spinner that scrolls with the page
+- Converted all the help sections into markdown
+- LDAP user filters
+- Streamline the content of notification emails (Pierre de La Morinerie)
+- Fixes a bug with group member administration (Matt DeTullio)
+- Sort tag names using VersionSorter (Robert Speicher)
+- Add GFM autocompletion for MergeRequests (Robert Speicher)
+- Add webhook when a new tag is pushed (Jeroen van Baarsen)
+- Add button for toggling inline comments in diff view
+- Add retry feature for repository import
+- Reuse the GitLab LDAP connection within each request
+- Changed markdown new line behaviour to conform to markdown standards
+- Fix global search
+- Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5)
+- Create and Update MR calls now support the description parameter (Greg Messner)
+- Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository
+- Added Slack service integration (Federico Ravasio)
+- Better API responses for access_levels (sponsored by O'Reilly Media)
+- Requires at least 2 unicorn workers
+- Requires gitlab-shell v1.9+
+- Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License)
+- Fix `/:username.keys` response content type (Dmitry Medvinsky)
+
+## 6.6.5
+
+- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
+- Hide mr close button for comment form if merge request was closed or inline comment
+- Adds ability to reopen closed merge request
+
+## 6.6.4
+
+- Add missing html escape for highlighted code blocks in comments, issues
+
+## 6.6.3
+
+- Fix 500 error when edit yourself from admin area
+- Hide private groups for public profiles
+
+## 6.6.2
+
+- Fix 500 error on branch/tag create or remove via UI
+
+## 6.6.1
+
+- Fix 500 error on files tab if submodules presents
+
+## 6.6.0
+
+- Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys
+- Permissions: Developer now can manage issue tracker (modify any issue)
+- Improve Code Compare page performance
+- Group avatar
+- Pygments.rb replaced with highlight.js
+- Improve Merge request diff store logic
+- Improve render performnace for MR show page
+- Fixed Assembla hardcoded project name
+- Jira integration documentation
+- Refactored app/services
+- Remove snippet expiration
+- Mobile UI improvements (Drew Blessing)
+- Fix block/remove UI for admin::users#show page
+- Show users' group membership on users' activity page (Robert Djurasaj)
+- User pages are visible without login if user is authorized to a public project
+- Markdown rendered headers have id derived from their name and link to their id
+- Improve application to work faster with large groups (100+ members)
+- Multiple emails per user
+- Show last commit for file when view file source
+- Restyle Issue#show page and MR#show page
+- Ability to filter by multiple labels for Issues page
+- Rails version to 4.0.3
+- Fixed attachment identifier displaying underneath note text (Jason Blanchard)
+
+## 6.5.1
+
+- Fix branch selectbox when create merge request from fork
+
+## 6.5.0
+
+- Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard)
+- Add color custimization and previewing to broadcast messages
+- Fixed notes anchors
+- Load new comments in issues dynamically
+- Added sort options to Public page
+- New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media)
+- Add project visibility icons to dashboard
+- Enable secure cookies if https used
+- Protect users/confirmation with rack_attack
+- Default HTTP headers to protect against MIME-sniffing, force https if enabled
+- Bootstrap 3 with responsive UI
+- New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth)
+- Restyled accept widgets for MR
+- SCSS refactored
+- Use jquery timeago plugin
+- Fix 500 error for rdoc files
+- Ability to customize merge commit message (sponsored by Say Media)
+- Search autocomplete via ajax
+- Add website url to user profile
+- Files API supports base64 encoded content (sponsored by O'Reilly Media)
+- Added support for Go's repository retrieval (Bruno Albuquerque)
+
+## 6.4.3
+
+- Don't use unicorn worker killer if PhusionPassenger is defined
+
+## 6.4.2
+
+- Fixed wrong behaviour of script/upgrade.rb
+
+## 6.4.1
+
+- Fixed bug with repository rename
+- Fixed bug with project transfer
+
+## 6.4.0
+
+- Added sorting to project issues page (Jason Blanchard)
+- Assembla integration (Carlos Paramio)
+- Fixed another 500 error with submodules
+- UI: More compact issues page
+- Minimal password length increased to 8 symbols
+- Side-by-side diff view (Steven Thonus)
+- Internal projects (Jason Hollingsworth)
+- Allow removal of avatar (Drew Blessing)
+- Project webhooks now support issues and merge request events
+- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
+- Expire event cache on avatar creation/removal (Drew Blessing)
+- Archiving old projects (Steven Thonus)
+- Rails 4
+- Add time ago tooltips to show actual date/time
+- UI: Fixed UI for admin system hooks
+- Ruby script for easier GitLab upgrade
+- Do not remove Merge requests if fork project was removed
+- Improve sign-in/signup UX
+- Add resend confirmation link to sign-in page
+- Set noreply@HOSTNAME for reply_to field in all emails
+- Show GitLab API version on Admin#dashboard
+- API Cross-origin resource sharing
+- Show READMe link at project home page
+- Show repo size for projects in Admin area
+
+## 6.3.0
+
+- API for adding gitlab-ci service
+- Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey)
+- Restyle project home page
+- Grammar fixes
+- Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev)
+- Security improvements
+- Added support for GitLab CI 4.0
+- Fixed issue with 500 error when group did not exist
+- Ability to leave project
+- You can create file in repo using UI
+- You can remove file from repo using UI
+- API: dropped default_branch attribute from project during creation
+- Project default_branch is not stored in db any more. It takes from repo now.
+- Admin broadcast messages
+- UI improvements
+- Dont show last push widget if user removed this branch
+- Fix 500 error for repos with newline in file name
+- Extended html titles
+- API: create/update/delete repo files
+- Admin can transfer project to any namespace
+- API: projects/all for admin users
+- Fix recent branches order
+
+## 6.2.4
+
+- Security: Cast API private_token to string (CVE-2013-4580)
+- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
+- Fix for Git SSH access for LDAP users
+
+## 6.2.3
+
+- Security: More protection against CVE-2013-4489
+- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
+- Fix sidekiq rake tasks
+
+## 6.2.2
+
+- Security: Update gitlab_git (CVE-2013-4489)
+
+## 6.2.1
+
+- Security: Fix issue with generated passwords for new users
+
+## 6.2.0
+
+- Public project pages are now visible to everyone (files, issues, wik, etc.)
+  THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE
+- Add group access to permissions page
+- Require current password to change one
+- Group owner or admin can remove other group owners
+- Remove group transfer since we have multiple owners
+- Respect authorization in Repository API
+- Improve UI for Project#files page
+- Add more security specs
+- Added search for projects by name to api (Izaak Alpert)
+- Make default user theme configurable (Izaak Alpert)
+- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
+- Rake tasks for webhooks management (Jonhnny Weslley)
+- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
+- API: Remove group
+- API: Remove project
+- Avatar upload on profile page with a maximum of 100KB (Steven Thonus)
+- Store the sessions in Redis instead of the cookie store
+- Fixed relative links in markdown
+- User must confirm their email if signup enabled
+- User must confirm changed email
+
+## 6.1.0
+
+- Project specific IDs for issues, mr, milestones
+  Above items will get a new id and for example all bookmarked issue urls will change.
+  Old issue urls are redirected to the new one if the issue id is too high for an internal id.
+- Description field added to Merge Request
+- API: Sudo api calls (Izaak Alpert)
+- API: Group membership api (Izaak Alpert)
+- Improved commit diff
+- Improved large commit handling (Boyan Tabakov)
+- Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey)
+- Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson)
+- Close issues automatically when pushing commits with a special message
+- Improve user removal from admin area
+- Invalidate events cache when project was moved
+- Remove deprecated classes and rake tasks
+- Add event filter for group and project show pages
+- Add links to create branch/tag from project home page
+- Add public-project? checkbox to new-project view
+- Improved compare page. Added link to proceed into Merge Request
+- Send an email to a user when they are added to group
+- New landing page when you have 0 projects
+
+## 6.0.0
+
+- Feature: Replace teams with group membership
+  We introduce group membership in 6.0 as a replacement for teams.
+  The old combination of groups and teams was confusing for a lot of people.
+  And when the members of a team where changed this wasn't reflected in the project permissions.
+  In GitLab 6.0 you will be able to add members to a group with a permission level for each member.
+  These group members will have access to the projects in that group.
+  Any changes to group members will immediately be reflected in the project permissions.
+  You can even have multiple owners for a group, greatly simplifying administration.
+- Feature: Ability to have multiple owners for group
+- Feature: Merge Requests between fork and project (Izaak Alpert)
+- Feature: Generate fingerprint for ssh keys
+- Feature: Ability to create and remove branches with UI
+- Feature: Ability to create and remove git tags with UI
+- Feature: Groups page in profile. You can leave group there
+- API: Allow login with LDAP credentials
+- Redesign: project settings navigation
+- Redesign: snippets area
+- Redesign: ssh keys page
+- Redesign: buttons, blocks and other ui elements
+- Add comment title to rss feed
+- You can use arrows to navigate at tree view
+- Add project filter on dashboard
+- Cache project graph
+- Drop support of root namespaces
+- Default theme is classic now
+- Cache result of methods like authorize_projects, project.team.members etc
+- Remove $.ready events
+- Fix onclick events being double binded
+- Add notification level to group membership
+- Move all project controllers/views under Projects:: module
+- Move all profile controllers/views under Profiles:: module
+- Apply user project limit only for personal projects
+- Unicorn is default web server again
+- Store satellites lock files inside satellites dir
+- Disabled threadsafety mode in rails
+- Fixed bug with loosing MR comments
+- Improved MR comments logic
+- Render readme file for projects in public area
+
+## 5.4.2
+
+- Security: Cast API private_token to string (CVE-2013-4580)
+- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
+
+## 5.4.1
+
+- Security: Fixes for CVE-2013-4489
+- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
+
+## 5.4.0
+
+- Ability to edit own comments
+- Documentation improvements
+- Improve dashboard projects page
+- Fixed nav for empty repos
+- GitLab Markdown help page
+- Misspelling fixes
+- Added support of unicorn and fog gems
+- Added client list to API doc
+- Fix PostgreSQL database restoration problem
+- Increase snippet content column size
+- allow project import via git:// url
+- Show participants on issues, including mentions
+- Notify mentioned users with email
+
+## 5.3.0
+
+- Refactored services
+- Campfire service added
+- HipChat service added
+- Fixed bug with LDAP + git over http
+- Fixed bug with google analytics code being ignored
+- Improve sign-in page if ldap enabled
+- Respect newlines in wall messages
+- Generate the Rails secret token on first run
+- Rename repo feature
+- Init.d: remove gitlab.socket on service start
+- Api: added teams api
+- Api: Prevent blob content being escaped
+- Api: Smart deploy key add behaviour
+- Api: projects/owned.json return user owned project
+- Fix bug with team assignation on project from #4109
+- Advanced snippets: public/private, project/personal (Andrew Kulakov)
+- Repository Graphs (Karlo Nicholas T. Soriano)
+- Fix dashboard lost if comment on commit
+- Update gitlab-grack. Fixes issue with --depth option
+- Fix project events duplicate on project page
+- Fix postgres error when displaying network graph.
+- Fix dashboard event filter when navigate via turbolinks
+- init.d: Ensure socket is removed before starting service
+- Admin area: Style teams:index, group:show pages
+- Own page for failed forking
+- Scrum view for milestone
+
+## 5.2.0
+
+- Turbolinks
+- Git over http with ldap credentials
+- Diff with better colors and some spacing on the corners
+- Default values for project features
+- Fixed huge_commit view
+- Restyle project clone panel
+- Move Gitlab::Git code to gitlab_git gem
+- Move update docs in repo
+- Requires gitlab-shell v1.4.0
+- Fixed submodules listing under file tab
+- Fork feature (Angus MacArthur)
+- git version check in gitlab:check
+- Shared deploy keys feature
+- Ability to generate default labels set for issues
+- Improve gfm autocomplete (Harold Luo)
+- Added support for Google Analytics
+- Code search feature (Javier Castro)
+
+## 5.1.0
+
+- You can login with email or username now
+- Corrected project transfer rollback when repository cannot be moved
+- Move both repo and wiki when project transfer requested
+- Admin area: project editing was removed from admin namespace
+- Access: admin user has now access to any project.
+- Notification settings
+- Gitlab::Git set of objects to abstract from grit library
+- Replace Unicorn web server with Puma
+- Backup/Restore refactored. Backup dump project wiki too now
+- Restyled Issues list. Show milestone version in issue row
+- Restyled Merge Request list
+- Backup now dump/restore uploads
+- Improved performance of dashboard (Andrew Kumanyaev)
+- File history now tracks renames (Akzhan Abdulin)
+- Drop wiki migration tools
+- Drop sqlite migration tools
+- project tagging
+- Paginate users in API
+- Restyled network graph (Hiroyuki Sato)
+
+## 5.0.1
+
+- Fixed issue with gitlab-grit being overridden by grit
+
+## 5.0.0
+
+- Replaced gitolite with gitlab-shell
+- Removed gitolite-related libraries
+- State machine added
+- Setup gitlab as git user
+- Internal API
+- Show team tab for empty projects
+- Import repository feature
+- Updated rails
+- Use lambda for scopes
+- Redesign admin area -> users
+- Redesign admin area -> user
+- Secure link to file attachments
+- Add validations for Group and Team names
+- Restyle team page for project
+- Update capybara, rspec-rails, poltergeist to recent versions
+- Wiki on git using Gollum
+- Added Solarized Dark theme for code review
+- Don't show user emails in autocomplete lists, profile pages
+- Added settings tab for group, team, project
+- Replace user popup with icons in header
+- Handle project moving with gitlab-shell
+- Added select2-rails for selectboxes with ajax data load
+- Fixed search field on projects page
+- Added teams to search autocomplete
+- Move groups and teams on dashboard sidebar to sub-tabs
+- API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell)
+- Redesign wall to be more like chat
+- Snippets, Wall features are disabled by default for new projects
+
+## 4.2.0
+
+- Teams
+- User show page. Via /u/username
+- Show help contents on pages for better navigation
+- Async gitolite calls
+- added satellites logs
+- can_create_group, can_create_team booleans for User
+- Process webhooks async
+- GFM: Fix images escaped inside links
+- Network graph improved
+- Switchable branches for network graph
+- API: Groups
+- Fixed project download
+
+## 4.1.0
+
+- Optional Sign-Up
+- Discussions
+- Satellites outside of tmp
+- Line numbers for blame
+- Project public mode
+- Public area with unauthorized access
+- Load dashboard events with ajax
+- remember dashboard filter in cookies
+- replace resque with sidekiq
+- fix routing issues
+- cleanup rake tasks
+- fix backup/restore
+- scss cleanup
+- show preview for note images
+- improved network-graph
+- get rid of app/roles/
+- added new classes Team, Repository
+- Reduce amount of gitolite calls
+- Ability to add user in all group projects
+- remove deprecated configs
+- replaced Korolev font with open font
+- restyled admin/dashboard page
+- restyled admin/projects page
+
+## 4.0.0
+
+- Remove project code and path from API. Use id instead
+- Return valid cloneable url to repo for webhook
+- Fixed backup issue
+- Reorganized settings
+- Fixed commits compare
+- Refactored scss
+- Improve status checks
+- Validates presence of User#name
+- Fixed postgres support
+- Removed sqlite support
+- Modified post-receive hook
+- Milestones can be closed now
+- Show comment events on dashboard
+- Quick add team members via group#people page
+- [API] expose created date for hooks and SSH keys
+- [API] list, create issue notes
+- [API] list, create snippet notes
+- [API] list, create wall notes
+- Remove project code - use path instead
+- added username field to user
+- rake task to fill usernames based on emails create namespaces for users
+- STI Group < Namespace
+- Project has namespace_id
+- Projects with namespaces also namespaced in gitolite and stored in subdir
+- Moving project to group will move it under group namespace
+- Ability to move project from namespaces to another
+- Fixes commit patches getting escaped (see #2036)
+- Support diff and patch generation for commits and merge request
+- MergeReqest doesn't generate a temporary file for the patch any more
+- Update the UI to allow downloading Patch or Diff
+
+## 3.1.0
+
+- Updated gems
+- Services: Gitlab CI integration
+- Events filter on dashboard
+- Own namespace for redis/resque
+- Optimized commit diff views
+- add alphabetical order for projects admin page
+- Improved web editor
+- Commit stats page
+- Documentation split and cleanup
+- Link to commit authors everywhere
+- Restyled milestones list
+- added Milestone to Merge Request
+- Restyled Top panel
+- Refactored Satellite Code
+- Added file line links
+- moved from capybara-webkit to poltergeist + phantomjs
+
+## 3.0.3
+
+- Fixed bug with issues list in Chrome
+- New Feature: Import team from another project
+
+## 3.0.2
+
+- Fixed gitlab:app:setup
+- Fixed application error on empty project in admin area
+- Restyled last push widget
+
+## 3.0.1
+
+- Fixed git over http
+
+## 3.0.0
+
+- Projects groups
+- Web Editor
+- Fixed bug with gitolite keys
+- UI improved
+- Increased performance of application
+- Show user avatar in last commit when browsing Files
+- Refactored Gitlab::Merge
+- Use Font Awesome for icons
+- Separate observing of Note and MergeRequests
+- Milestone "All Issues" filter
+- Fix issue close and reopen button text and styles
+- Fix forward/back while browsing Tree hierarchy
+- Show number of notes for commits and merge requests
+- Added support pg from box and update installation doc
+- Reject ssh keys that break gitolite
+- [API] list one project hook
+- [API] edit project hook
+- [API] list project snippets
+- [API] allow to authorize using private token in HTTP header
+- [API] add user creation
+
+## 2.9.1
+
+- Fixed resque custom config init
+
+## 2.9.0
+
+- fixed inline notes bugs
+- refactored rspecs
+- refactored gitolite backend
+- added factory_girl
+- restyled projects list on dashboard
+- ssh keys validation to prevent gitolite crash
+- send notifications if changed permission in project
+- scss refactoring. gitlab_bootstrap/ dir
+- fix git push http body bigger than 112k problem
+- list of labels  page under issues tab
+- API for milestones, keys
+- restyled buttons
+- OAuth
+- Comment order changed
+
+## 2.8.1
+
+- ability to disable gravatars
+- improved MR diff logic
+- ssh key help page
+
+## 2.8.0
+
+- Gitlab Flavored Markdown
+- Bulk issues update
+- Issues API
+- Cucumber coverage increased
+- Post-receive files fixed
+- UI improved
+- Application cleanup
+- more cucumber
+- capybara-webkit + headless
+
+## 2.7.0
+
+- Issue Labels
+- Inline diff
+- Git HTTP
+- API
+- UI improved
+- System hooks
+- UI improved
+- Dashboard events endless scroll
+- Source performance increased
+
+## 2.6.0
+
+- UI polished
+- Improved network graph + keyboard nav
+- Handle huge commits
+- Last Push widget
+- Bugfix
+- Better performance
+- Email in resque
+- Increased test coverage
+- Ability to remove branch with MR accept
+- a lot of code refactored
+
+## 2.5.0
+
+- UI polished
+- Git blame for file
+- Bugfix
+- Email in resque
+- Better test coverage
+
+## 2.4.0
+
+- Admin area stats page
+- Ability to block user
+- Simplified dashboard area
+- Improved admin area
+- Bootstrap 2.0
+- Responsive layout
+- Big commits handling
+- Performance improved
+- Milestones
+
+## 2.3.1
+
+- Issues pagination
+- ssl fixes
+- Merge Request pagination
+
+## 2.3.0
+
+- Dashboard r1
+- Search r1
+- Project page
+- Close merge request on push
+- Persist MR diff after merge
+- mysql support
+- Documentation
+
+## 2.2.0
+
+- We’ve added support of LDAP auth
+- Improved permission logic (4 roles system)
+- Protected branches (now only masters can push to protected branches)
+- Usability improved
+- twitter bootstrap integrated
+- compare view between commits
+- wiki feature
+- now you can enable/disable issues, wiki, wall features per project
+- security fixes
+- improved code browsing (ajax branch switch etc)
+- improved per-line commenting
+- git submodules displayed
+- moved to rails 3.2
+- help section improved
+
+## 2.1.0
+
+- Project tab r1
+- List branches/tags
+- per line comments
+- mass user import
+
+## 2.0.0
+
+- gitolite as main git host system
+- merge requests
+- project/repo access
+- link to commit/issue feed
+- design tab
+- improved email notifications
+- restyled dashboard
+- bugfix
+
+## 1.2.2
+
+- common config file gitlab.yml
+- issues restyle
+- snippets restyle
+- clickable news feed header on dashboard
+- bugfix
+
+## 1.2.1
+
+- bugfix
+
+## 1.2.0
+
+- new design
+- user dashboard
+- network graph
+- markdown support for comments
+- encoding issues
+- wall like twitter timeline
+
+## 1.1.0
+
+- project dashboard
+- wall redesigned
+- feature: code snippets
+- fixed horizontal scroll on file preview
+- fixed app crash if commit message has invalid chars
+- bugfix & code cleaning
+
+## 1.0.2
+
+- fixed bug with empty project
+- added adv validation for project path & code
+- feature: issues can be sortable
+- bugfix
+- username displayed on top panel
+
+## 1.0.1
+
+- fixed: with invalid source code for commit
+- fixed: lose branch/tag selection when use tree navigation
+- when history clicked - display path
+- bug fix & code cleaning
+
+## 1.0.0
+
+- bug fix
+- projects preview mode
+
+## 0.9.6
+
+- css fix
+- new repo empty tree until restart server - fixed
+
+## 0.9.4
+
+- security improved
+- authorization improved
+- html escaping
+- bug fix
+- increased test coverage
+- design improvements
+
+## 0.9.1
+
+- increased test coverage
+- design improvements
+- new issue email notification
+- updated app name
+- issue redesigned
+- issue can be edit
+
+## 0.8.0
+
+- syntax highlight for main file types
+- redesign
+- stability
+- security fixes
+- increased test coverage
+- email notification
diff --git a/app/mailers/.gitkeep b/changelogs/unreleased/.gitkeep
similarity index 100%
rename from app/mailers/.gitkeep
rename to changelogs/unreleased/.gitkeep
diff --git a/changelogs/unreleased/feature-api_owned_resource.yml b/changelogs/unreleased/feature-api_owned_resource.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9c270e4ecf4056c285b6ad94f41105cd5028714c
--- /dev/null
+++ b/changelogs/unreleased/feature-api_owned_resource.yml
@@ -0,0 +1,4 @@
+---
+title: Add api endpoint `/groups/owned`
+merge_request: 7103
+author: Borja Aparicio
diff --git a/changelogs/unreleased/fix-cache-for-commit-status.yml b/changelogs/unreleased/fix-cache-for-commit-status.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eb4e96e75aea38515550168a0382398f60140ee7
--- /dev/null
+++ b/changelogs/unreleased/fix-cache-for-commit-status.yml
@@ -0,0 +1,4 @@
+---
+title: Fix cache for commit status in commits list to respect branches
+merge_request: 7372
+author:
diff --git a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad6aa214f0fbff13eac9ba496d6fb8783813bf54
--- /dev/null
+++ b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml
@@ -0,0 +1,4 @@
+---
+title: Fix error when using invalid branch name when creating a new pipeline
+merge_request: 7324
+author: 
diff --git a/changelogs/unreleased/forking-in-progress-title.yml b/changelogs/unreleased/forking-in-progress-title.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b9684844b32b29b360cc275b5e7a669a2f3834f
--- /dev/null
+++ b/changelogs/unreleased/forking-in-progress-title.yml
@@ -0,0 +1,4 @@
+---
+title: Use 'Forking in progress' title when appropriate
+merge_request: 7394
+author: Philip Karpiak
diff --git a/config/application.rb b/config/application.rb
index 4a9ed41cbf88a751b92552933b42f8ff1a761974..946b632b0e8093050046da34fdb0185a64514ccf 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -24,7 +24,8 @@ module Gitlab
                                      #{config.root}/app/models/ci
                                      #{config.root}/app/models/hooks
                                      #{config.root}/app/models/members
-                                     #{config.root}/app/models/project_services))
+                                     #{config.root}/app/models/project_services
+                                     #{config.root}/app/workers/concerns))
 
     config.generators.templates.push("#{config.root}/generator_templates")
 
@@ -50,6 +51,7 @@ module Gitlab
     # - Build variables (:variables)
     # - GitLab Pages SSL cert/key info (:certificate, :encrypted_key)
     # - Webhook URLs (:hook)
+    # - GitLab-shell secret token (:secret_token)
     # - Sentry DSN (:sentry_dsn)
     # - Deploy keys (:key)
     config.filter_parameters += %i(
@@ -62,6 +64,7 @@ module Gitlab
       password
       password_confirmation
       private_token
+      secret_token
       sentry_dsn
       variables
     )
@@ -85,6 +88,14 @@ module Gitlab
     config.assets.precompile << "users/users_bundle.js"
     config.assets.precompile << "network/network_bundle.js"
     config.assets.precompile << "profile/profile_bundle.js"
+    config.assets.precompile << "protected_branches/protected_branches_bundle.js"
+    config.assets.precompile << "diff_notes/diff_notes_bundle.js"
+    config.assets.precompile << "boards/boards_bundle.js"
+    config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
+    config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
+    config.assets.precompile << "boards/test_utils/simulate_drag.js"
+    config.assets.precompile << "blob_edit/blob_edit_bundle.js"
+    config.assets.precompile << "snippet/snippet_bundle.js"
     config.assets.precompile << "lib/utils/*.js"
     config.assets.precompile << "lib/*.js"
     config.assets.precompile << "u2f.js"
@@ -94,13 +105,24 @@ module Gitlab
 
     config.action_view.sanitized_allowed_protocols = %w(smb)
 
-    config.middleware.use Rack::Attack
+    config.middleware.insert_before Warden::Manager, Rack::Attack
 
     # Allow access to GitLab API from other domains
-    config.middleware.use Rack::Cors do
+    config.middleware.insert_before Warden::Manager, Rack::Cors do
+      allow do
+        origins Gitlab.config.gitlab.url
+        resource '/api/*',
+          credentials: true,
+          headers: :any,
+          methods: :any,
+          expose: ['Link']
+      end
+
+      # Cross-origin requests must not have the session cookie available
       allow do
         origins '*'
         resource '/api/*',
+          credentials: false,
           headers: :any,
           methods: :any,
           expose: ['Link']
@@ -111,6 +133,10 @@ module Gitlab
     redis_config_hash = Gitlab::Redis.params
     redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
     redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
+    if Sidekiq.server? # threaded context
+      redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5
+      redis_config_hash[:pool_timeout] = 1
+    end
     config.cache_store = :redis_store, redis_config_hash
 
     config.active_record.raise_in_transactional_callbacks = true
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 74325872b09713cc318e1d94d94ff72176adc7e1..c11296975b7c4d92402495f86b6de803f085d2c9 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -101,6 +101,13 @@
     :why: GPL-licensed libraries cannot be linked to from non-GPL projects.
     :versions: []
     :when: 2016-05-02 05:29:43.904715000 Z
+- - :blacklist
+  - OSL-3.0
+  - :who: Sean McGivern
+    :why: The OSL license is a copyleft license
+    :versions: []
+    :when: 2016-10-28 11:02:15.540105000 Z
+
 
 # GEM LICENSES
 - - :license
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 1470a6e2550baf123c40c5765fb6dad942c08169..699ab6075b6e7a13a60b0ee299d90833225096f3 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -70,6 +70,7 @@ production: &base
     email_from: example@example.com
     email_display_name: GitLab
     email_reply_to: noreply@example.com
+    email_subject_suffix: ''
 
     # Email server smtp settings are in config/initializers/smtp_settings.rb.sample
 
@@ -111,7 +112,7 @@ production: &base
 
   ## Reply by email
   # Allow users to comment on issues and merge requests by replying to notification emails.
-  # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html
+  # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
   incoming_email:
     enabled: false
 
@@ -431,7 +432,9 @@ production: &base
   ## Repositories settings
   repositories:
     # Paths where repositories can be stored. Give the canonicalized absolute pathname.
-    # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
+    # IMPORTANT: None of the path components may be symlink, because
+    # 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/
 
@@ -546,6 +549,10 @@ test:
       project_url: "http://redmine/projects/:issues_tracker_id"
       issues_url: "http://redmine/:project_id/:issues_tracker_id/:id"
       new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new"
+    jira:
+      title: "JIRA"
+      url: https://sample_company.atlasian.net
+      project_key: PROJECT
   ldap:
     enabled: false
     servers:
diff --git a/config/initializers/0_post_deployment_migrations.rb b/config/initializers/0_post_deployment_migrations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0068a03d21416d2769a5a1fff0808994a4077445
--- /dev/null
+++ b/config/initializers/0_post_deployment_migrations.rb
@@ -0,0 +1,12 @@
+# Post deployment migrations are included by default. This file must be loaded
+# before other initializers as Rails may otherwise memoize a list of migrations
+# excluding the post deployment migrations.
+unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
+  path = Rails.root.join('db', 'post_migrate').to_s
+
+  Rails.application.config.paths['db/migrate'] << path
+
+  # Rails memoizes migrations at certain points where it won't read the above
+  # path just yet. As such we must also update the following list of paths.
+  ActiveRecord::Migrator.migrations_paths << path
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index deac3b0f0f97b539dc3d05aadb68e3da711f20d7..9fec2ad6bf77018513ea8a65e1de26f795245ec2 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni
 Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
 Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
 Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
+Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
 Settings.gitlab['base_url']   ||= Settings.send(:build_base_gitlab_url)
 Settings.gitlab['url']        ||= Settings.send(:build_gitlab_url)
 Settings.gitlab['user']       ||= 'git'
@@ -212,7 +213,7 @@ Settings.gitlab.default_projects_features['builds']             = true if Settin
 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)
 Settings.gitlab['domain_whitelist'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
 Settings.gitlab['trusted_proxies'] ||= []
 
 #
@@ -293,6 +294,22 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
 Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
 Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
+Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
+Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
+Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
+
+Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
+Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
 
 #
 # GitLab Shell
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae2ca258df188ac76c939623dde3b32c46f1e53e
--- /dev/null
+++ b/config/initializers/7_redis.rb
@@ -0,0 +1,3 @@
+# Make sure we initialize a Redis connection pool before Sidekiq starts
+# multi-threaded execution.
+Gitlab::Redis.with { nil }
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35e8b3808e246cd66401c498b2906aa6c84a03a7
--- /dev/null
+++ b/config/initializers/ar5_batching.rb
@@ -0,0 +1,41 @@
+# Port ActiveRecord::Relation#in_batches from ActiveRecord 5.
+# https://github.com/rails/rails/blob/ac027338e4a165273607dccee49a3d38bc836794/activerecord/lib/active_record/relation/batches.rb#L184
+# TODO: this can be removed once we're using AR5.
+raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
+
+module ActiveRecord
+  module Batches
+    # Differences from upstream: enumerator support was removed, and custom
+    # order/limit clauses are ignored without a warning.
+    def in_batches(of: 1000, start: nil, finish: nil, load: false)
+      raise "Must provide a block" unless block_given?
+
+      relation = self.reorder(batch_order).limit(of)
+      relation = relation.where(arel_table[primary_key].gteq(start)) if start
+      relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
+      batch_relation = relation
+
+      loop do
+        if load
+          records = batch_relation.records
+          ids = records.map(&:id)
+          yielded_relation = self.where(primary_key => ids)
+          yielded_relation.load_records(records)
+        else
+          ids = batch_relation.pluck(primary_key)
+          yielded_relation = self.where(primary_key => ids)
+        end
+
+        break if ids.empty?
+
+        primary_key_offset = ids.last
+        raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
+
+        yield yielded_relation
+
+        break if ids.length < of
+        batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
+      end
+    end
+  end
+end
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0da584626eeaef2bc4e0f91f4c1bb7b712f0d85c
--- /dev/null
+++ b/config/initializers/ar_monkey_patch.rb
@@ -0,0 +1,57 @@
+# rubocop:disable Lint/RescueException
+
+# This patch fixes https://github.com/rails/rails/issues/26024
+# TODO: Remove it when it's no longer necessary
+
+module ActiveRecord
+  module Locking
+    module Optimistic
+      # We overwrite this method because we don't want to have default value
+      # for newly created records
+      def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+        super
+      end
+
+      def _update_record(attribute_names = self.attribute_names) #:nodoc:
+        return super unless locking_enabled?
+        return 0 if attribute_names.empty?
+
+        lock_col = self.class.locking_column
+
+        previous_lock_value = send(lock_col).to_i
+
+        # This line is added as a patch
+        previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
+
+        increment_lock
+
+        attribute_names += [lock_col]
+        attribute_names.uniq!
+
+        begin
+          relation = self.class.unscoped
+
+          affected_rows = relation.where(
+            self.class.primary_key => id,
+            lock_col => previous_lock_value,
+          ).update_all(
+            attributes_for_update(attribute_names).map do |name|
+              [name, _read_attribute(name)]
+            end.to_h
+          )
+
+          unless affected_rows == 1
+            raise ActiveRecord::StaleObjectError.new(self, "update")
+          end
+
+          affected_rows
+
+        # If something went wrong, revert the version.
+        rescue Exception
+          send(lock_col + '=', previous_lock_value)
+          raise
+        end
+      end
+    end
+  end
+end
diff --git a/config/initializers/ar_speed_up_migration_checking.rb b/config/initializers/ar_speed_up_migration_checking.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1fe5defc01dc8da1ea05d769e1f9f6a5306da48a
--- /dev/null
+++ b/config/initializers/ar_speed_up_migration_checking.rb
@@ -0,0 +1,18 @@
+if Rails.env.test?
+  require 'active_record/migration'
+
+  module ActiveRecord
+    class Migrator
+      class << self
+        alias_method :migrations_unmemoized, :migrations
+
+        # This method is called a large number of times per rspec example, and
+        # it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5
+        # seconds per spec.
+        def migrations(paths)
+          @migrations ||= migrations_unmemoized(paths)
+        end
+      end
+    end
+  end
+end
diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb
index c668864089be109118ff38a29b841a1d8d5990ba..e007666b852e85db5eda2767dc8b88e442c1e893 100644
--- a/config/initializers/attr_encrypted_no_db_connection.rb
+++ b/config/initializers/attr_encrypted_no_db_connection.rb
@@ -1,20 +1,21 @@
 module AttrEncrypted
   module Adapters
     module ActiveRecord
-      def attribute_instance_methods_as_symbols_with_no_db_connection
-        # Use with_connection so the connection doesn't stay pinned to the thread.
-        connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
-        
-        if connected
-          # Call version from AttrEncrypted::Adapters::ActiveRecord
-          attribute_instance_methods_as_symbols_without_no_db_connection
-        else
-          # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
-          AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+      module DBConnectionQuerier
+        def attribute_instance_methods_as_symbols
+          # Use with_connection so the connection doesn't stay pinned to the thread.
+          connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
+
+          if connected
+            # Call version from AttrEncrypted::Adapters::ActiveRecord
+            super
+          else
+            # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
+            AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+          end
         end
       end
-
-      alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection
+      prepend DBConnectionQuerier
     end
   end
 end
diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb
index d831a1838edaa49264cd032f9e43059c33af9663..d0b1444f60791bd2f99506ab8509133c913acca3 100644
--- a/config/initializers/connection_fix.rb
+++ b/config/initializers/connection_fix.rb
@@ -20,7 +20,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
         execute_without_retry(*args)
       rescue ActiveRecord::StatementInvalid => e
         if e.message =~ /server has gone away/i
-          warn "Server timed out, retrying"
+          warn "Lost connection to MySQL server during query"
           reconnect!
           retry
         else
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 618dba74151b52275cfdaba7698f82c3ba496710..fc4b0a72addf4190b96ce3e38a2dfc143dcff239 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -12,7 +12,8 @@ Doorkeeper.configure do
   end
 
   resource_owner_from_credentials do |routes|
-    Gitlab::Auth.find_with_user_password(params[:username], params[:password])
+    user = Gitlab::Auth.find_with_user_password(params[:username], params[:password])
+    user unless user.try(:two_factor_enabled?)
   end
 
   # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb
index 7454c33c9ddafd5ee0694b415eb602722096d109..529dcdd4644907f713e66e40c3b4a425fdad02ea 100644
--- a/config/initializers/gitlab_shell_secret_token.rb
+++ b/config/initializers/gitlab_shell_secret_token.rb
@@ -1 +1 @@
-Gitlab::Shell.new.generate_and_link_secret_token
+Gitlab::Shell.ensure_secret_token!
diff --git a/config/initializers/gitlab_workhorse_secret.rb b/config/initializers/gitlab_workhorse_secret.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed54dc11098724e7d7e84dcf0a0e638e6b9dab30
--- /dev/null
+++ b/config/initializers/gitlab_workhorse_secret.rb
@@ -0,0 +1,8 @@
+begin
+  Gitlab::Workhorse.secret
+rescue
+  Gitlab::Workhorse.write_secret
+end
+
+# Try a second time. If it does not work this will raise.
+Gitlab::Workhorse.secret
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 52522e099e7de06c3bd9a3588f43332f8fdadd08..3b8771543e4fb5f1a1500cb1071924aca5f8388b 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -67,8 +67,10 @@ if Gitlab::Metrics.enabled?
       ['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', 'email', 'message'] => ['lib'],
+      ['lib', 'gitlab', 'checks']           => ['lib']
     }
 
     paths_to_instrument.each do |(path, prefix)|
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index f498732feca21222c6c0dc921b3482463a35a7f0..5e3e4c966cbbf5013b763c34c05f6b85e63db6c8 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -13,9 +13,5 @@ Mime::Type.register "video/mp4",  :mp4, [], [:m4v, :mov]
 Mime::Type.register "video/webm", :webm
 Mime::Type.register "video/ogg",  :ogv
 
-middlewares = Gitlab::Application.config.middleware
-middlewares.swap(ActionDispatch::ParamsParser, ActionDispatch::ParamsParser, {
-  Mime::Type.lookup('application/vnd.git-lfs+json') => lambda do |body|
-    ActiveSupport::JSON.decode(body)
-  end
-})
+Mime::Type.unregister :json
+Mime::Type.register 'application/json', :json, %w(application/vnd.git-lfs+json application/json)
diff --git a/config/initializers/postgresql_limit_fix.rb b/config/initializers/postgresql_limit_fix.rb
index 0cb3aaf4d24c9b674d23cfa352850c3f7ff0ee1c..4224d857e8abeda5dad2e4b88fb78d5336051da7 100644
--- a/config/initializers/postgresql_limit_fix.rb
+++ b/config/initializers/postgresql_limit_fix.rb
@@ -1,5 +1,19 @@
 if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
   class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+    module LimitFilter
+      def add_column(table_name, column_name, type, options = {})
+        options.delete(:limit) if type == :text
+        super(table_name, column_name, type, options)
+      end
+
+      def change_column(table_name, column_name, type, options = {})
+        options.delete(:limit) if type == :text
+        super(table_name, column_name, type, options)
+      end
+    end
+
+    prepend ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::LimitFilter
+
     class TableDefinition
       def text(*args)
         options = args.extract_options!
@@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
         column_names.each { |name| column(name, type, options) }
       end
     end
-
-    def add_column_with_limit_filter(table_name, column_name, type, options = {})
-      options.delete(:limit) if type == :text
-      add_column_without_limit_filter(table_name, column_name, type, options)
-    end
-
-    def change_column_with_limit_filter(table_name, column_name, type, options = {})
-      options.delete(:limit) if type == :text
-      change_column_without_limit_filter(table_name, column_name, type, options)
-    end
-
-    alias_method_chain :add_column, :limit_filter
-    alias_method_chain :change_column, :limit_filter
   end
 end
diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb
new file mode 100644
index 0000000000000000000000000000000000000000..25003cf0239045f497a9153b8a7c286ecce18a2f
--- /dev/null
+++ b/config/initializers/routing_draw.rb
@@ -0,0 +1,7 @@
+# Adds draw method into Rails routing
+# It allows us to keep routing splitted into files
+class ActionDispatch::Routing::Mapper
+  def draw(routes_name)
+    instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
+  end
+end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 74fef7cadfe24904b5d1425ce7506df5d95856d8..4f30d1265c89e118f4438388295ccbd16599eed3 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -18,6 +18,9 @@ if Rails.env.production?
       
       # Sanitize fields based on those sanitized from Rails.
       config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
+      # Sanitize authentication headers
+      config.sanitize_http_headers = %w[Authorization Private-Token]
+      config.tags = { program: Gitlab::Sentry.program_context }
     end
   end
 end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f7e714cd6bc7855a48e268b984a8f3a64e1c4c38..023af2af23caa75b2e1cd0f725c8fdf123221f3c 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -2,6 +2,9 @@
 redis_config_hash = Gitlab::Redis.params
 redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE
 
+# Default is to retry 25 times with exponential backoff. That's too much.
+Sidekiq.default_worker_options = { retry: 3 }
+
 Sidekiq.configure_server do |config|
   config.redis = redis_config_hash
 
@@ -42,3 +45,19 @@ end
 Sidekiq.configure_client do |config|
   config.redis = redis_config_hash
 end
+
+# The Sidekiq client API always adds the queue to the Sidekiq queue
+# list, but mail_room and gitlab-shell do not. This is only necessary
+# for monitoring.
+config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+
+begin
+  Sidekiq.redis do |conn|
+    conn.pipelined do
+      config[:queues].each do |queue|
+        conn.sadd('queues', queue[0])
+      end
+    end
+  end
+rescue Redis::BaseError, SocketError
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cedb5e207bd3798f0ff2a8e6651937ba368d2b8a..12a59be79f07d6257cdf6262ddcc33e84de12af6 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -5,6 +5,7 @@ en:
   hello: "Hello world"
   errors:
     messages:
+      label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
       wrong_size: "is the wrong size (should be %{file_size})"
       size_too_small: "is too small (should be at least %{file_size})"
       size_too_big: "is too big (should be at most %{file_size})"
diff --git a/config/mail_room.yml b/config/mail_room.yml
index c639f8260aa1b2785ca0e044fe370051623f9230..b026d510f1b513fabde9317b615254e40b9eaf63 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -25,12 +25,27 @@
       :delivery_options:
         :redis_url: <%= config[:redis_url].to_json %>
         :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
-        :queue: incoming_email
+        :queue: email_receiver
         :worker: EmailReceiverWorker
+        <% if config[:sentinels] %>
+        :sentinels:
+          <% config[:sentinels].each do |sentinel| %>
+          -
+            :host: <%= sentinel[:host] %>
+            :port: <%= sentinel[:port] %>
+          <% end %>
+        <% end %>
 
       :arbitration_method: redis
       :arbitration_options:
         :redis_url: <%= config[:redis_url].to_json %>
         :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %>
-
+        <% if config[:sentinels] %>
+        :sentinels:
+          <% config[:sentinels].each do |sentinel| %>
+          -
+            :host: <%= sentinel[:host] %>
+            :port: <%= sentinel[:port] %>
+          <% end %>
+        <% end %>
   <% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 63a8827a6a23cf8aae051ee17ffc5c8638a1d11a..7bf6c03e69b4e974543eb972312f5f6711f7871a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,54 +3,19 @@ require 'sidekiq/cron/web'
 require 'api/api'
 
 Rails.application.routes.draw do
-  if Gitlab::Sherlock.enabled?
-    namespace :sherlock do
-      resources :transactions, only: [:index, :show] do
-        resources :queries, only: [:show]
-        resources :file_samples, only: [:show]
-
-        collection do
-          delete :destroy_all
-        end
-      end
-    end
-  end
-
-  if Rails.env.development?
-    # Make the built-in Rails routes available in development, otherwise they'd
-    # get swallowed by the `namespace/project` route matcher below.
-    #
-    # See https://git.io/va79N
-    get '/rails/mailers'         => 'rails/mailers#index'
-    get '/rails/mailers/:path'   => 'rails/mailers#preview'
-    get '/rails/info/properties' => 'rails/info#properties'
-    get '/rails/info/routes'     => 'rails/info#routes'
-    get '/rails/info'            => 'rails/info#index'
-
-    mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
-  end
-
   concern :access_requestable do
     post :request_access, on: :collection
     post :approve_access_request, on: :member
   end
 
-  namespace :ci do
-    # CI API
-    Ci::API::API.logger Rails.logger
-    mount Ci::API::API => '/api'
-
-    resource :lint, only: [:show, :create]
-
-    resources :projects, only: [:index, :show] do
-      member do
-        get :status, to: 'projects#badge'
-      end
-    end
-
-    root to: 'projects#index'
+  concern :awardable do
+    post :toggle_award_emoji, on: :member
   end
 
+  draw :sherlock
+  draw :development
+  draw :ci
+
   use_doorkeeper do
     controllers applications: 'oauth/applications',
                 authorized_applications: 'oauth/authorized_applications',
@@ -72,40 +37,18 @@ Rails.application.routes.draw do
   # JSON Web Token
   get 'jwt/auth' => 'jwt#auth'
 
-  # API
-  API::API.logger Rails.logger
-  mount API::API => '/api'
-
-  constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
-  constraints constraint do
-    mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
-  end
-
   # Health check
   get 'health_check(/:checks)' => 'health_check#index', as: :health_check
 
-  # Help
-  get 'help'           => 'help#index'
-  get 'help/shortcuts' => 'help#shortcuts'
-  get 'help/ui'        => 'help#ui'
-  get 'help/*path'     => 'help#show', as: :help_page
+  # Koding route
+  get 'koding' => 'koding#index'
 
-  #
-  # Global snippets
-  #
-  resources :snippets do
-    member do
-      get 'raw'
-    end
-  end
-
-  get '/s/:username', to: redirect('/u/%{username}/snippets'),
-                      constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
+  draw :api
+  draw :sidekiq
+  draw :help
+  draw :snippets
 
-  #
   # Invites
-  #
-
   resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
     member do
       post :accept
@@ -119,779 +62,26 @@ Rails.application.routes.draw do
     end
   end
 
-  #
   # Spam reports
-  #
   resources :abuse_reports, only: [:new, :create]
 
-  #
   # Notification settings
-  #
   resources :notification_settings, only: [:create, :update]
 
-  #
-  # Import
-  #
-  namespace :import do
-    resource :github, only: [:create, :new], controller: :github do
-      post :personal_access_token
-      get :status
-      get :callback
-      get :jobs
-    end
-
-    resource :gitlab, only: [:create], controller: :gitlab do
-      get :status
-      get :callback
-      get :jobs
-    end
-
-    resource :bitbucket, only: [:create], controller: :bitbucket do
-      get :status
-      get :callback
-      get :jobs
-    end
-
-    resource :gitorious, only: [:create, :new], controller: :gitorious do
-      get :status
-      get :callback
-      get :jobs
-    end
-
-    resource :google_code, only: [:create, :new], controller: :google_code do
-      get :status
-      post :callback
-      get :jobs
-
-      get   :new_user_map,    path: :user_map
-      post  :create_user_map, path: :user_map
-    end
-
-    resource :fogbugz, only: [:create, :new], controller: :fogbugz do
-      get :status
-      post :callback
-      get :jobs
-
-      get   :new_user_map,    path: :user_map
-      post  :create_user_map, path: :user_map
-    end
-
-    resource :gitlab_project, only: [:create, :new] do
-      post :create
-    end
-  end
-
-  #
-  # Uploads
-  #
-
-  scope path: :uploads do
-    # Note attachments and User/Group/Project avatars
-    get ":model/:mounted_as/:id/:filename",
-        to:           "uploads#show",
-        constraints:  { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
-
-    # Appearance
-    get ":model/:mounted_as/:id/:filename",
-        to:           "uploads#show",
-        constraints:  { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
-
-    # Project markdown uploads
-    get ":namespace_id/:project_id/:secret/:filename",
-      to:           "projects/uploads#show",
-      constraints:  { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
-  end
-
-  # Redirect old note attachments path to new uploads path.
-  get "files/note/:id/:filename",
-    to:           redirect("uploads/note/attachment/%{id}/%{filename}"),
-    constraints:  { filename: /[^\/]+/ }
-
-  #
-  # Explore area
-  #
-  namespace :explore do
-    resources :projects, only: [:index] do
-      collection do
-        get :trending
-        get :starred
-      end
-    end
-
-    resources :groups, only: [:index]
-    resources :snippets, only: [:index]
-    root to: 'projects#trending'
-  end
-
-  # Compatibility with old routing
-  get 'public' => 'explore/projects#index'
-  get 'public/projects' => 'explore/projects#index'
-
-  #
-  # Admin Area
-  #
-  namespace :admin do
-    resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
-      resources :keys, only: [:show, :destroy]
-      resources :identities, except: [:show]
-
-      member do
-        get :projects
-        get :keys
-        get :groups
-        put :block
-        put :unblock
-        put :unlock
-        put :confirm
-        post :impersonate
-        patch :disable_two_factor
-        delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
-      end
-    end
-
-    resource :impersonation, only: :destroy
-
-    resources :abuse_reports, only: [:index, :destroy]
-    resources :spam_logs, only: [:index, :destroy] do
-      member do
-        post :mark_as_ham
-      end
-    end
-
-    resources :applications
-
-    resources :groups, constraints: { id: /[^\/]+/ } do
-      member do
-        put :members_update
-      end
-    end
-
-    resources :deploy_keys, only: [:index, :new, :create, :destroy]
-
-    resources :hooks, only: [:index, :create, :destroy] do
-      get :test
-    end
-
-    resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
-      post :preview, on: :collection
-    end
-
-    resource :logs, only: [:show]
-    resource :health_check, controller: 'health_check', only: [:show]
-    resource :background_jobs, controller: 'background_jobs', only: [:show]
-    resource :system_info, controller: 'system_info', only: [:show]
-    resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
-
-    resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
-      root to: 'projects#index', as: :projects
-
-      resources(:projects,
-                path: '/',
-                constraints: { id: /[a-zA-Z.0-9_\-]+/ },
-                only: [:index, :show]) do
-        root to: 'projects#show'
-
-        member do
-          put :transfer
-          post :repository_check
-        end
-
-        resources :runner_projects, only: [:create, :destroy]
-      end
-    end
-
-    resource :appearances, only: [:show, :create, :update], path: 'appearance' do
-      member do
-        get :preview
-        delete :logo
-        delete :header_logos
-      end
-    end
-
-    resource :application_settings, only: [:show, :update] do
-      resources :services, only: [:index, :edit, :update]
-      put :reset_runners_token
-      put :reset_health_check_token
-      put :clear_repository_check_states
-    end
-
-    resources :labels
-
-    resources :runners, only: [:index, :show, :update, :destroy] do
-      member do
-        get :resume
-        get :pause
-      end
-    end
-
-    resources :builds, only: :index do
-      collection do
-        post :cancel_all
-      end
-    end
-
-    root to: 'dashboard#index'
-  end
-
-  #
-  # Profile Area
-  #
-  resource :profile, only: [:show, :update] do
-    member do
-      get :audit_log
-      get :applications, to: 'oauth/applications#index'
-
-      put :reset_private_token
-      put :update_username
-    end
-
-    scope module: :profiles do
-      resource :account, only: [:show] do
-        member do
-          delete :unlink
-        end
-      end
-      resource :notifications, only: [:show, :update]
-      resource :password, only: [:new, :create, :edit, :update] do
-        member do
-          put :reset
-        end
-      end
-      resource :preferences, only: [:show, :update]
-      resources :keys, only: [:index, :show, :new, :create, :destroy]
-      resources :emails, only: [:index, :create, :destroy]
-      resource :avatar, only: [:destroy]
-
-      resources :personal_access_tokens, only: [:index, :create] do
-        member do
-          put :revoke
-        end
-      end
-
-      resource :two_factor_auth, only: [:show, :create, :destroy] do
-        member do
-          post :create_u2f
-          post :codes
-          patch :skip
-        end
-      end
-    end
-  end
-
-  scope(path: 'u/:username',
-        as: :user,
-        constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
-        controller: :users) do
-    get :calendar
-    get :calendar_activities
-    get :groups
-    get :projects
-    get :contributed, as: :contributed_projects
-    get :snippets
-    get '/', action: :show
-  end
-
-  #
-  # Dashboard Area
-  #
-  resource :dashboard, controller: 'dashboard', only: [] do
-    get :issues
-    get :merge_requests
-    get :activity
-
-    scope module: :dashboard do
-      resources :milestones, only: [:index, :show]
-      resources :labels, only: [:index]
-
-      resources :groups, only: [:index]
-      resources :snippets, only: [:index]
-
-      resources :todos, only: [:index, :destroy] do
-        collection do
-          delete :destroy_all
-        end
-      end
-
-      resources :projects, only: [:index] do
-        collection do
-          get :starred
-        end
-      end
-    end
-
-    root to: "dashboard/projects#index"
-  end
-
-  #
-  # Groups Area
-  #
-  resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }  do
-    member do
-      get :issues
-      get :merge_requests
-      get :projects
-      get :activity
-    end
-
-    scope module: :groups do
-      resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
-        post :resend_invite, on: :member
-        delete :leave, on: :collection
-      end
-
-      resource :avatar, only: [:destroy]
-      resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
-    end
-  end
-
-  resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
-
-  devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
-                                    registrations: :registrations,
-                                    passwords: :passwords,
-                                    sessions: :sessions,
-                                    confirmations: :confirmations }
-
-  devise_scope :user do
-    get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
-    get '/users/almost_there' => 'confirmations#almost_there'
-  end
-
-  root to: "root#index"
-
-  #
-  # Project Area
-  #
-  resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
-    resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
-              [:new, :create, :index], path: "/") do
-      member do
-        put :transfer
-        delete :remove_fork
-        post :archive
-        post :unarchive
-        post :housekeeping
-        post :toggle_star
-        post :preview_markdown
-        post :export
-        post :remove_export
-        post :generate_new_export
-        get :download_export
-        get :autocomplete_sources
-        get :activity
-        get :refs
-      end
-
-      scope module: :projects do
-        scope constraints: { id: /.+\.git/, format: nil } do
-          # Git HTTP clients ('git clone' etc.)
-          get '/info/refs', to: 'git_http#info_refs'
-          post '/git-upload-pack', to: 'git_http#git_upload_pack'
-          post '/git-receive-pack', to: 'git_http#git_receive_pack'
-
-          # Git LFS API (metadata)
-          post '/info/lfs/objects/batch', to: 'lfs_api#batch'
-          post '/info/lfs/objects', to: 'lfs_api#deprecated'
-          get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
-
-          # GitLab LFS object storage
-          scope constraints: { oid: /[a-f0-9]{64}/ } do
-            get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
-
-            scope constraints: { size: /[0-9]+/ } do
-              put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
-              put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
-            end
-          end
-        end
-
-        # Allow /info/refs, /info/refs?service=git-upload-pack, and
-        # /info/refs?service=git-receive-pack, but nothing else.
-        #
-        git_http_handshake = lambda do |request|
-          request.query_string.blank? ||
-            request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
-        end
-
-        ref_redirect = redirect do |params, request|
-          path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
-          path << "?#{request.query_string}" unless request.query_string.blank?
-          path
-        end
-
-        get '/info/refs', constraints: git_http_handshake, to: ref_redirect
-
-        # Blob routes:
-        get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
-        post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
-        get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
-        put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
-        post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
-
-        #
-        # Templates
-        #
-        get '/templates/:template_type/:key' => 'templates#show', as: :template
-
-        scope do
-          get(
-            '/blob/*id/diff',
-            to: 'blob#diff',
-            constraints: { id: /.+/, format: false },
-            as: :blob_diff
-          )
-          get(
-            '/blob/*id',
-            to: 'blob#show',
-            constraints: { id: /.+/, format: false },
-            as: :blob
-          )
-          delete(
-            '/blob/*id',
-            to: 'blob#destroy',
-            constraints: { id: /.+/, format: false }
-          )
-          put(
-            '/blob/*id',
-            to: 'blob#update',
-            constraints: { id: /.+/, format: false }
-          )
-          post(
-            '/blob/*id',
-            to: 'blob#create',
-            constraints: { id: /.+/, format: false }
-          )
-        end
-
-        scope do
-          get(
-            '/raw/*id',
-            to: 'raw#show',
-            constraints: { id: /.+/, format: /(html|js)/ },
-            as: :raw
-          )
-        end
-
-        scope do
-          get(
-            '/tree/*id',
-            to: 'tree#show',
-            constraints: { id: /.+/, format: /(html|js)/ },
-            as: :tree
-          )
-        end
-
-        scope do
-          get(
-            '/find_file/*id',
-            to: 'find_file#show',
-            constraints: { id: /.+/, format: /html/ },
-            as: :find_file
-          )
-        end
-
-        scope do
-          get(
-            '/files/*id',
-            to: 'find_file#list',
-            constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
-            as: :files
-          )
-        end
-
-        scope do
-          post(
-            '/create_dir/*id',
-              to: 'tree#create_dir',
-              constraints: { id: /.+/ },
-              as: 'create_dir'
-          )
-        end
-
-        scope do
-          get(
-            '/blame/*id',
-            to: 'blame#show',
-            constraints: { id: /.+/, format: /(html|js)/ },
-            as: :blame
-          )
-        end
-
-        scope do
-          get(
-            '/commits/*id',
-            to: 'commits#show',
-            constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
-            as: :commits
-          )
-        end
-
-        resource  :avatar, only: [:show, :destroy]
-        resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
-          member do
-            get :branches
-            get :builds
-            post :cancel_builds
-            post :retry_builds
-            post :revert
-            post :cherry_pick
-            get :diff_for_path
-          end
-        end
-
-        resources :compare, only: [:index, :create] do
-          collection do
-            get :diff_for_path
-          end
-        end
-
-        get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
-
-        # Don't use format parameter as file extension (old 3.0.x behavior)
-        # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
-        scope format: false do
-          resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
-
-          resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
-            member do
-              get :commits
-              get :ci
-              get :languages
-            end
-          end
-        end
-
-        resources :snippets, constraints: { id: /\d+/ } do
-          member do
-            get 'raw'
-          end
-        end
-
-        WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
-
-        scope do
-          # Order matters to give priority to these matches
-          get '/wikis/git_access', to: 'wikis#git_access'
-          get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
-          post '/wikis', to: 'wikis#create'
-
-          get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
-          get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
-
-          get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
-          delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
-          put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
-          post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
-        end
-
-        resource :repository, only: [:create] do
-          member do
-            get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
-          end
-        end
-
-        resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
-          member do
-            get :test
-          end
-        end
-
-        resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
-          member do
-            put :enable
-            put :disable
-          end
-        end
-
-        resources :forks, only: [:index, :new, :create]
-        resource :import, only: [:new, :create, :show]
-
-        resources :refs, only: [] do
-          collection do
-            get 'switch'
-          end
-
-          member do
-            # tree viewer logs
-            get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
-            # Directories with leading dots erroneously get rejected if git
-            # ref regex used in constraints. Regex verification now done in controller.
-            get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
-              id: /.*/,
-              path: /.*/
-            }
-          end
-        end
-
-        resources :merge_requests, constraints: { id: /\d+/ } do
-          member do
-            get :commits
-            get :diffs
-            get :builds
-            get :merge_check
-            post :merge
-            post :cancel_merge_when_build_succeeds
-            get :ci_status
-            post :toggle_subscription
-            post :toggle_award_emoji
-            post :remove_wip
-            get :diff_for_path
-          end
-
-          collection do
-            get :branch_from
-            get :branch_to
-            get :update_branches
-            get :diff_for_path
-          end
-        end
-
-        resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
-        resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
-          resource :release, only: [:edit, :update]
-        end
-
-        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 :pipelines, only: [:index, :new, :create, :show] do
-          collection do
-            resource :pipelines_settings, path: 'settings', only: [:show, :update]
-          end
-
-          member do
-            post :cancel
-            post :retry
-          end
-        end
-
-        resources :environments
-
-        resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
-          collection do
-            post :cancel_all
-          end
-
-          member do
-            get :status
-            post :cancel
-            post :retry
-            post :play
-            post :erase
-            get :trace
-            get :raw
-          end
-
-          resource :artifacts, only: [] do
-            get :download
-            get :browse, path: 'browse(/*path)', format: false
-            get :file, path: 'file/*path', format: false
-            post :keep
-          end
-        end
-
-        resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
-          member do
-            get :test
-          end
-        end
-
-        resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
-
-        resources :milestones, constraints: { id: /\d+/ } do
-          member do
-            put :sort_issues
-            put :sort_merge_requests
-          end
-        end
-
-        resources :labels, except: [:show], constraints: { id: /\d+/ } do
-          collection do
-            post :generate
-            post :set_priorities
-          end
-
-          member do
-            post :toggle_subscription
-            delete :remove_priority
-          end
-        end
-
-        resources :issues, constraints: { id: /\d+/ } do
-          member do
-            post :toggle_subscription
-            post :toggle_award_emoji
-            post :mark_as_spam
-            get :referenced_merge_requests
-            get :related_branches
-            get :can_create_branch
-          end
-          collection do
-            post  :bulk_update
-          end
-        end
-
-        resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
-          collection do
-            delete :leave
-
-            # Used for import team
-            # from another project
-            get :import
-            post :apply_import
-          end
-
-          member do
-            post :resend_invite
-          end
-        end
-
-        resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
-
-        resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
-          member do
-            post :toggle_award_emoji
-            delete :delete_attachment
-          end
-        end
-
-        resources :todos, only: [:create]
-
-        resources :uploads, only: [:create] do
-          collection do
-            get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
-          end
-        end
-
-        resources :runners, only: [:index, :edit, :update, :destroy, :show] do
-          member do
-            get :resume
-            get :pause
-          end
-
-          collection do
-            post :toggle_shared_runners
-          end
-        end
-
-        resources :runner_projects, only: [:create, :destroy]
-        resources :badges, only: [:index] do
-          collection do
-            scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
-              constraints format: /svg/ do
-                get :build
-                get :coverage
-              end
-            end
-          end
-        end
-      end
-    end
-  end
+  draw :import
+  draw :uploads
+  draw :explore
+  draw :admin
+  draw :profile
+  draw :dashboard
+  draw :group
+  draw :user
+  draw :project
 
   # Get all keys of user
   get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
 
-  get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
+  root to: "root#index"
+
+  get '*unmatched_route', to: 'application#not_found'
 end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ae985da56107ab0ad465af2710b763a771b7849
--- /dev/null
+++ b/config/routes/admin.rb
@@ -0,0 +1,102 @@
+namespace :admin do
+  resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
+    resources :keys, only: [:show, :destroy]
+    resources :identities, except: [:show]
+
+    member do
+      get :projects
+      get :keys
+      get :groups
+      put :block
+      put :unblock
+      put :unlock
+      put :confirm
+      post :impersonate
+      patch :disable_two_factor
+      delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
+    end
+  end
+
+  resource :impersonation, only: :destroy
+
+  resources :abuse_reports, only: [:index, :destroy]
+  resources :spam_logs, only: [:index, :destroy] do
+    member do
+      post :mark_as_ham
+    end
+  end
+
+  resources :applications
+
+  resources :groups, constraints: { id: /[^\/]+/ } do
+    member do
+      put :members_update
+    end
+  end
+
+  resources :deploy_keys, only: [:index, :new, :create, :destroy]
+
+  resources :hooks, only: [:index, :create, :destroy] do
+    get :test
+  end
+
+  resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
+    post :preview, on: :collection
+  end
+
+  resource :logs, only: [:show]
+  resource :health_check, controller: 'health_check', only: [:show]
+  resource :background_jobs, controller: 'background_jobs', only: [:show]
+  resource :system_info, controller: 'system_info', only: [:show]
+  resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
+
+  resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+    root to: 'projects#index', as: :projects
+
+    resources(:projects,
+              path: '/',
+              constraints: { id: /[a-zA-Z.0-9_\-]+/ },
+              only: [:index, :show]) do
+      root to: 'projects#show'
+
+      member do
+        put :transfer
+        post :repository_check
+      end
+
+      resources :runner_projects, only: [:create, :destroy]
+    end
+  end
+
+  resource :appearances, only: [:show, :create, :update], path: 'appearance' do
+    member do
+      get :preview
+      delete :logo
+      delete :header_logos
+    end
+  end
+
+  resource :application_settings, only: [:show, :update] do
+    resources :services, only: [:index, :edit, :update]
+    put :reset_runners_token
+    put :reset_health_check_token
+    put :clear_repository_check_states
+  end
+
+  resources :labels
+
+  resources :runners, only: [:index, :show, :update, :destroy] do
+    member do
+      get :resume
+      get :pause
+    end
+  end
+
+  resources :builds, only: :index do
+    collection do
+      post :cancel_all
+    end
+  end
+
+  root to: 'dashboard#index'
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69c8efc151c9c51b2d7e4415d1f262396c019906
--- /dev/null
+++ b/config/routes/api.rb
@@ -0,0 +1,2 @@
+API::API.logger Rails.logger
+mount API::API => '/api'
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
new file mode 100644
index 0000000000000000000000000000000000000000..47a049d5b204381e417668808aebb9c79b201703
--- /dev/null
+++ b/config/routes/ci.rb
@@ -0,0 +1,15 @@
+namespace :ci do
+  # CI API
+  Ci::API::API.logger Rails.logger
+  mount Ci::API::API => '/api'
+
+  resource :lint, only: [:show, :create]
+
+  resources :projects, only: [:index, :show] do
+    member do
+      get :status, to: 'projects#badge'
+    end
+  end
+
+  root to: 'projects#index'
+end
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fb20c63bc631ea7b7ac899a385573a1e063247fd
--- /dev/null
+++ b/config/routes/dashboard.rb
@@ -0,0 +1,27 @@
+resource :dashboard, controller: 'dashboard', only: [] do
+  get :issues
+  get :merge_requests
+  get :activity
+
+  scope module: :dashboard do
+    resources :milestones, only: [:index, :show]
+    resources :labels, only: [:index]
+
+    resources :groups, only: [:index]
+    resources :snippets, only: [:index]
+
+    resources :todos, only: [:index, :destroy] do
+      collection do
+        delete :destroy_all
+      end
+    end
+
+    resources :projects, only: [:index] do
+      collection do
+        get :starred
+      end
+    end
+  end
+
+  root to: "dashboard/projects#index"
+end
diff --git a/config/routes/development.rb b/config/routes/development.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b2b47c6a2129acf76f402d989c484fbde7b2cd6
--- /dev/null
+++ b/config/routes/development.rb
@@ -0,0 +1,13 @@
+if Rails.env.development?
+  # Make the built-in Rails routes available in development, otherwise they'd
+  # get swallowed by the `namespace/project` route matcher below.
+  #
+  # See https://git.io/va79N
+  get '/rails/mailers'         => 'rails/mailers#index'
+  get '/rails/mailers/:path'   => 'rails/mailers#preview'
+  get '/rails/info/properties' => 'rails/info#properties'
+  get '/rails/info/routes'     => 'rails/info#routes'
+  get '/rails/info'            => 'rails/info#index'
+
+  mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
+end
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
new file mode 100644
index 0000000000000000000000000000000000000000..42ec5e8abec1da5f54e2826c12c6f0aa21fb098b
--- /dev/null
+++ b/config/routes/explore.rb
@@ -0,0 +1,16 @@
+namespace :explore do
+  resources :projects, only: [:index] do
+    collection do
+      get :trending
+      get :starred
+    end
+  end
+
+  resources :groups, only: [:index]
+  resources :snippets, only: [:index]
+  root to: 'projects#trending'
+end
+
+# Compatibility with old routing
+get 'public' => 'explore/projects#index'
+get 'public/projects' => 'explore/projects#index'
diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03adc4815f38be4204cbe0762bac4b8341cb7a3f
--- /dev/null
+++ b/config/routes/git_http.rb
@@ -0,0 +1,37 @@
+scope constraints: { id: /.+\.git/, format: nil } do
+  # Git HTTP clients ('git clone' etc.)
+  get '/info/refs', to: 'git_http#info_refs'
+  post '/git-upload-pack', to: 'git_http#git_upload_pack'
+  post '/git-receive-pack', to: 'git_http#git_receive_pack'
+
+  # Git LFS API (metadata)
+  post '/info/lfs/objects/batch', to: 'lfs_api#batch'
+  post '/info/lfs/objects', to: 'lfs_api#deprecated'
+  get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+
+  # GitLab LFS object storage
+  scope constraints: { oid: /[a-f0-9]{64}/ } do
+    get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+
+    scope constraints: { size: /[0-9]+/ } do
+      put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
+      put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+    end
+  end
+end
+
+# Allow /info/refs, /info/refs?service=git-upload-pack, and
+# /info/refs?service=git-receive-pack, but nothing else.
+#
+git_http_handshake = lambda do |request|
+  request.query_string.blank? ||
+    request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
+end
+
+ref_redirect = redirect do |params, request|
+  path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+  path << "?#{request.query_string}" unless request.query_string.blank?
+  path
+end
+
+get '/info/refs', constraints: git_http_handshake, to: ref_redirect
diff --git a/config/routes/group.rb b/config/routes/group.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c392f77ef67746734666a2c72a54d4ac5059690
--- /dev/null
+++ b/config/routes/group.rb
@@ -0,0 +1,37 @@
+require 'constraints/group_url_constrainer'
+
+constraints(GroupUrlConstrainer.new) do
+  scope(path: ':id',
+        as: :group,
+        constraints: { id: Gitlab::Regex.namespace_route_regex },
+        controller: :groups) do
+    get '/', action: :show
+    patch '/', action: :update
+    put '/', action: :update
+    delete '/', action: :destroy
+  end
+end
+
+resources :groups, only: [:index, :new, :create]
+
+scope(path: 'groups/:id', controller: :groups) do
+  get :edit, as: :edit_group
+  get :issues, as: :issues_group
+  get :merge_requests, as: :merge_requests_group
+  get :projects, as: :projects_group
+  get :activity, as: :activity_group
+end
+
+scope(path: 'groups/:group_id', module: :groups, as: :group) do
+  resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+    post :resend_invite, on: :member
+    delete :leave, on: :collection
+  end
+
+  resource :avatar, only: [:destroy]
+  resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+  resources :labels, except: [:show], constraints: { id: /\d+/ }
+end
+
+# Must be last route in this file
+get 'groups/:id' => 'groups#show', as: :group_canonical
diff --git a/config/routes/help.rb b/config/routes/help.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d53822da9eca4ab0bb8828303cd5223dba6ade37
--- /dev/null
+++ b/config/routes/help.rb
@@ -0,0 +1,4 @@
+get 'help'           => 'help#index'
+get 'help/shortcuts' => 'help#shortcuts'
+get 'help/ui'        => 'help#ui'
+get 'help/*path'     => 'help#show', as: :help_page
diff --git a/config/routes/import.rb b/config/routes/import.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89f3b3f6378019cd827d1607d3c19e6bfbea6cca
--- /dev/null
+++ b/config/routes/import.rb
@@ -0,0 +1,42 @@
+namespace :import do
+  resource :github, only: [:create, :new], controller: :github do
+    post :personal_access_token
+    get :status
+    get :callback
+    get :jobs
+  end
+
+  resource :gitlab, only: [:create], controller: :gitlab do
+    get :status
+    get :callback
+    get :jobs
+  end
+
+  resource :bitbucket, only: [:create], controller: :bitbucket do
+    get :status
+    get :callback
+    get :jobs
+  end
+
+  resource :google_code, only: [:create, :new], controller: :google_code do
+    get :status
+    post :callback
+    get :jobs
+
+    get   :new_user_map,    path: :user_map
+    post  :create_user_map, path: :user_map
+  end
+
+  resource :fogbugz, only: [:create, :new], controller: :fogbugz do
+    get :status
+    post :callback
+    get :jobs
+
+    get   :new_user_map,    path: :user_map
+    post  :create_user_map, path: :user_map
+  end
+
+  resource :gitlab_project, only: [:create, :new] do
+    post :create
+  end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
new file mode 100644
index 0000000000000000000000000000000000000000..52b9a565db8654ccce3746057e4b99df7fe00dc8
--- /dev/null
+++ b/config/routes/profile.rb
@@ -0,0 +1,44 @@
+resource :profile, only: [:show, :update] do
+  member do
+    get :audit_log
+    get :applications, to: 'oauth/applications#index'
+
+    put :reset_private_token
+    put :reset_incoming_email_token
+    put :update_username
+  end
+
+  scope module: :profiles do
+    resource :account, only: [:show] do
+      member do
+        delete :unlink
+      end
+    end
+    resource :notifications, only: [:show, :update]
+    resource :password, only: [:new, :create, :edit, :update] do
+      member do
+        put :reset
+      end
+    end
+    resource :preferences, only: [:show, :update]
+    resources :keys, only: [:index, :show, :new, :create, :destroy]
+    resources :emails, only: [:index, :create, :destroy]
+    resource :avatar, only: [:destroy]
+
+    resources :personal_access_tokens, only: [:index, :create] do
+      member do
+        put :revoke
+      end
+    end
+
+    resource :two_factor_auth, only: [:show, :create, :destroy] do
+      member do
+        post :create_u2f
+        post :codes
+        patch :skip
+      end
+    end
+
+    resources :u2f_registrations, only: [:destroy]
+  end
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
new file mode 100644
index 0000000000000000000000000000000000000000..82defb0ba7150baea051d5b9254cdae08e04a860
--- /dev/null
+++ b/config/routes/project.rb
@@ -0,0 +1,302 @@
+resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
+
+resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+  resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
+            [:new, :create, :index], path: "/") do
+    member do
+      put :transfer
+      delete :remove_fork
+      post :archive
+      post :unarchive
+      post :housekeeping
+      post :toggle_star
+      post :preview_markdown
+      post :export
+      post :remove_export
+      post :generate_new_export
+      get :download_export
+      get :autocomplete_sources
+      get :activity
+      get :refs
+      put :new_issue_address
+    end
+
+    scope module: :projects do
+      draw :git_http
+
+      #
+      # Templates
+      #
+      get '/templates/:template_type/:key' => 'templates#show', as: :template
+
+      resource  :avatar, only: [:show, :destroy]
+      resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
+        member do
+          get :branches
+          get :builds
+          get :pipelines
+          post :cancel_builds
+          post :retry_builds
+          post :revert
+          post :cherry_pick
+          get :diff_for_path
+        end
+      end
+
+      resources :compare, only: [:index, :create] do
+        collection do
+          get :diff_for_path
+        end
+      end
+
+      get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+      # Don't use format parameter as file extension (old 3.0.x behavior)
+      # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
+      scope format: false do
+        resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
+
+        resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
+          member do
+            get :commits
+            get :ci
+            get :languages
+          end
+        end
+      end
+
+      resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
+        member do
+          get 'raw'
+        end
+      end
+
+      resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
+        member do
+          get :test
+        end
+      end
+
+      resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
+        member do
+          put :enable
+          put :disable
+        end
+      end
+
+      resources :forks, only: [:index, :new, :create]
+      resource :import, only: [:new, :create, :show]
+
+      resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
+        member do
+          get :commits
+          get :diffs
+          get :conflicts
+          get :conflict_for_path
+          get :builds
+          get :pipelines
+          get :merge_check
+          post :merge
+          post :cancel_merge_when_build_succeeds
+          get :ci_status
+          get :ci_environments_status
+          post :toggle_subscription
+          post :remove_wip
+          get :diff_for_path
+          post :resolve_conflicts
+          post :assign_related_issues
+        end
+
+        collection do
+          get :branch_from
+          get :branch_to
+          get :update_branches
+          get :diff_for_path
+          post :bulk_update
+          get :new_diffs, path: 'new/diffs'
+        end
+
+        resources :discussions, only: [], constraints: { id: /\h{40}/ } do
+          member do
+            post :resolve
+            delete :resolve, action: :unresolve
+          end
+        end
+      end
+
+      resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+      resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
+        resource :release, only: [:edit, :update]
+      end
+
+      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 :pipelines, only: [:index, :new, :create, :show] do
+        collection do
+          resource :pipelines_settings, path: 'settings', only: [:show, :update]
+        end
+
+        member do
+          post :cancel
+          post :retry
+        end
+      end
+
+      resources :environments, except: [:destroy] do
+        member do
+          post :stop
+        end
+      end
+
+      resource :cycle_analytics, only: [:show]
+
+      resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
+        collection do
+          post :cancel_all
+
+          resources :artifacts, only: [] do
+            collection do
+              get :latest_succeeded,
+                path: '*ref_name_and_path',
+                format: false
+            end
+          end
+        end
+
+        member do
+          get :status
+          post :cancel
+          post :retry
+          post :play
+          post :erase
+          get :trace
+          get :raw
+        end
+
+        resource :artifacts, only: [] do
+          get :download
+          get :browse, path: 'browse(/*path)', format: false
+          get :file, path: 'file/*path', format: false
+          post :keep
+        end
+      end
+
+      resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
+        member do
+          get :test
+        end
+      end
+
+      resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+
+      resources :milestones, constraints: { id: /\d+/ } do
+        member do
+          put :sort_issues
+          put :sort_merge_requests
+        end
+      end
+
+      resources :labels, except: [:show], constraints: { id: /\d+/ } do
+        collection do
+          post :generate
+          post :set_priorities
+        end
+
+        member do
+          post :toggle_subscription
+          delete :remove_priority
+        end
+      end
+
+      resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
+        member do
+          post :toggle_subscription
+          post :mark_as_spam
+          get :referenced_merge_requests
+          get :related_branches
+          get :can_create_branch
+        end
+        collection do
+          post  :bulk_update
+        end
+      end
+
+      resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
+        collection do
+          delete :leave
+
+          # Used for import team
+          # from another project
+          get :import
+          post :apply_import
+        end
+
+        member do
+          post :resend_invite
+        end
+      end
+
+      resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
+
+      resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+        member do
+          delete :delete_attachment
+          post :resolve
+          delete :resolve, action: :unresolve
+        end
+      end
+
+      resources :boards, only: [:index, :show] do
+        scope module: :boards do
+          resources :issues, only: [:update]
+
+          resources :lists, only: [:index, :create, :update, :destroy] do
+            collection do
+              post :generate
+            end
+
+            resources :issues, only: [:index, :create]
+          end
+        end
+      end
+
+      resources :todos, only: [:create]
+
+      resources :uploads, only: [:create] do
+        collection do
+          get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+        end
+      end
+
+      resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+        member do
+          get :resume
+          get :pause
+        end
+
+        collection do
+          post :toggle_shared_runners
+        end
+      end
+
+      resources :runner_projects, only: [:create, :destroy]
+      resources :badges, only: [:index] do
+        collection do
+          scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+            constraints format: /svg/ do
+              get :build
+              get :coverage
+            end
+          end
+        end
+      end
+
+      # Since both wiki and repository routing contains wildcard characters
+      # its preferable to keep it below all other project routes
+      draw :wiki
+      draw :repository
+    end
+  end
+end
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
new file mode 100644
index 0000000000000000000000000000000000000000..76dcf113aea6dd9ef2fae24498369d4e815f57a3
--- /dev/null
+++ b/config/routes/repository.rb
@@ -0,0 +1,110 @@
+# All routing related to repositoty browsing
+
+resource :repository, only: [:create] do
+  member do
+    get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
+  end
+end
+
+resources :refs, only: [] do
+  collection do
+    get 'switch'
+  end
+
+  member do
+    # tree viewer logs
+    get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+    # Directories with leading dots erroneously get rejected if git
+    # ref regex used in constraints. Regex verification now done in controller.
+    get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
+      id: /.*/,
+      path: /.*/
+    }
+  end
+end
+
+get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
+post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
+get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
+put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
+post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+
+scope do
+  get(
+    '/blob/*id/diff',
+    to: 'blob#diff',
+    constraints: { id: /.+/, format: false },
+    as: :blob_diff
+  )
+  get(
+    '/blob/*id',
+    to: 'blob#show',
+    constraints: { id: /.+/, format: false },
+    as: :blob
+  )
+  delete(
+    '/blob/*id',
+    to: 'blob#destroy',
+    constraints: { id: /.+/, format: false }
+  )
+  put(
+    '/blob/*id',
+    to: 'blob#update',
+    constraints: { id: /.+/, format: false }
+  )
+  post(
+    '/blob/*id',
+    to: 'blob#create',
+    constraints: { id: /.+/, format: false }
+  )
+
+  get(
+    '/raw/*id',
+    to: 'raw#show',
+    constraints: { id: /.+/, format: /(html|js)/ },
+    as: :raw
+  )
+
+  get(
+    '/tree/*id',
+    to: 'tree#show',
+    constraints: { id: /.+/, format: /(html|js)/ },
+    as: :tree
+  )
+
+  get(
+    '/find_file/*id',
+    to: 'find_file#show',
+    constraints: { id: /.+/, format: /html/ },
+    as: :find_file
+  )
+
+  get(
+    '/files/*id',
+    to: 'find_file#list',
+    constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+    as: :files
+  )
+
+  post(
+    '/create_dir/*id',
+      to: 'tree#create_dir',
+      constraints: { id: /.+/ },
+      as: 'create_dir'
+  )
+
+  get(
+    '/blame/*id',
+    to: 'blame#show',
+    constraints: { id: /.+/, format: /(html|js)/ },
+    as: :blame
+  )
+
+  # File/dir history
+  get(
+    '/commits/*id',
+    to: 'commits#show',
+    constraints: { id: /.+/, format: false },
+    as: :commits
+  )
+end
diff --git a/config/routes/sherlock.rb b/config/routes/sherlock.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c9969f91c36eb3d2998d93501add36ae06646d65
--- /dev/null
+++ b/config/routes/sherlock.rb
@@ -0,0 +1,12 @@
+if Gitlab::Sherlock.enabled?
+  namespace :sherlock do
+    resources :transactions, only: [:index, :show] do
+      resources :queries, only: [:show]
+      resources :file_samples, only: [:show]
+
+      collection do
+        delete :destroy_all
+      end
+    end
+  end
+end
diff --git a/config/routes/sidekiq.rb b/config/routes/sidekiq.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d3e6bc4c2926794e3b728c9ece131c0da395b63e
--- /dev/null
+++ b/config/routes/sidekiq.rb
@@ -0,0 +1,4 @@
+constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
+constraints constraint do
+  mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
+end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ca096f31ba6ba13cae95337022148a1a73a3b16
--- /dev/null
+++ b/config/routes/snippets.rb
@@ -0,0 +1,9 @@
+resources :snippets, concerns: :awardable do
+  member do
+    get 'raw'
+    get 'download'
+  end
+end
+
+get '/s/:username', to: redirect('/u/%{username}/snippets'),
+                    constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b22148a1343090756b53ce553a2e8bae4da77f9
--- /dev/null
+++ b/config/routes/uploads.rb
@@ -0,0 +1,21 @@
+scope path: :uploads do
+  # Note attachments and User/Group/Project avatars
+  get ":model/:mounted_as/:id/:filename",
+      to:           "uploads#show",
+      constraints:  { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+
+  # Appearance
+  get ":model/:mounted_as/:id/:filename",
+      to:           "uploads#show",
+      constraints:  { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+
+  # Project markdown uploads
+  get ":namespace_id/:project_id/:secret/:filename",
+    to:           "projects/uploads#show",
+    constraints:  { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
+end
+
+# Redirect old note attachments path to new uploads path.
+get "files/note/:id/:filename",
+  to:           redirect("uploads/note/attachment/%{id}/%{filename}"),
+  constraints:  { filename: /[^\/]+/ }
diff --git a/config/routes/user.rb b/config/routes/user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc1068af6f632658bc9ea447eab0d00e7d569e3e
--- /dev/null
+++ b/config/routes/user.rb
@@ -0,0 +1,45 @@
+require 'constraints/user_url_constrainer'
+
+devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
+                                  registrations: :registrations,
+                                  passwords: :passwords,
+                                  sessions: :sessions,
+                                  confirmations: :confirmations }
+
+devise_scope :user do
+  get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
+  get '/users/almost_there' => 'confirmations#almost_there'
+end
+
+constraints(UserUrlConstrainer.new) do
+  scope(path: ':username',
+        as: :user,
+        constraints: { username: Gitlab::Regex.namespace_route_regex },
+        controller: :users) do
+    get '/', action: :show
+  end
+end
+
+scope(constraints: { username: Gitlab::Regex.namespace_route_regex }) do
+  scope(path: 'users/:username',
+        as: :user,
+        controller: :users) do
+    get :calendar
+    get :calendar_activities
+    get :groups
+    get :projects
+    get :contributed, as: :contributed_projects
+    get :snippets
+    get :exists
+    get '/', to: redirect('/%{username}')
+  end
+
+  # Compatibility with old routing
+  # TODO (dzaporozhets): remove in 10.0
+  get '/u/:username', to: redirect('/%{username}')
+  # TODO (dzaporozhets): remove in 9.0
+  get '/u/:username/groups', to: redirect('/users/%{username}/groups')
+  get '/u/:username/projects', to: redirect('/users/%{username}/projects')
+  get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
+  get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
+end
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ecd4d395d668277aafa7fcc57f18534e1f086477
--- /dev/null
+++ b/config/routes/wiki.rb
@@ -0,0 +1,16 @@
+WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+
+scope do
+  # Order matters to give priority to these matches
+  get '/wikis/git_access', to: 'wikis#git_access'
+  get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+  post '/wikis', to: 'wikis#create'
+
+  get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
+  get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
+
+  get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
+  delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
+  put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
+  post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0aec8aedf724ffd979ef88ac6043bdc9d63cb97c
--- /dev/null
+++ b/config/sidekiq_queues.yml
@@ -0,0 +1,48 @@
+# This configuration file should be exclusively used to set queue settings for
+# Sidekiq. Any other setting should be specified using the Sidekiq CLI or the
+# Sidekiq Ruby API (see config/initializers/sidekiq.rb).
+---
+# All the queues to process and their weights. Every queue _must_ have a weight
+# defined.
+#
+# The available weights are as follows
+#
+# 1: low priority
+# 2: medium priority
+# 3: high priority
+# 5: _super_ high priority, this should only be used for _very_ important queues
+#
+# As per http://stackoverflow.com/a/21241357/290102 the formula for calculating
+# the likelihood of a job being popped off a queue (given all queues have work
+# to perform) is:
+#
+#     chance = (queue weight / total weight of all queues) * 100
+:queues:
+  - [post_receive, 5]
+  - [merge, 5]
+  - [update_merge_requests, 3]
+  - [process_commit, 2]
+  - [new_note, 2]
+  - [build, 2]
+  - [pipeline, 2]
+  - [gitlab_shell, 2]
+  - [email_receiver, 2]
+  - [emails_on_push, 2]
+  - [mailers, 2]
+  - [repository_fork, 1]
+  - [repository_import, 1]
+  - [project_service, 1]
+  - [clear_database_cache, 1]
+  - [delete_user, 1]
+  - [expire_build_instance_artifacts, 1]
+  - [group_destroy, 1]
+  - [irker, 1]
+  - [project_cache, 1]
+  - [project_destroy, 1]
+  - [project_export, 1]
+  - [project_web_hook, 1]
+  - [repository_check, 1]
+  - [system_hook, 1]
+  - [git_garbage_collect, 1]
+  - [cronjob, 1]
+  - [default, 1]
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index e3316ecdb6cc5463ed04c6bc0c9cbee3f45a93b1..a984eda5ab5900a1b80de211903510a29f9f530d 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -3,11 +3,11 @@ require 'sidekiq/testing'
 Sidekiq::Testing.inline! do
   Gitlab::Seeder.quiet do
     project_urls = [
-      'https://github.com/documentcloud/underscore.git',
+      'https://gitlab.com/gitlab-org/gitlab-test.git',
       'https://gitlab.com/gitlab-org/gitlab-ce.git',
       'https://gitlab.com/gitlab-org/gitlab-ci.git',
       'https://gitlab.com/gitlab-org/gitlab-shell.git',
-      'https://gitlab.com/gitlab-org/gitlab-test.git',
+      'https://github.com/documentcloud/underscore.git',
       'https://github.com/twitter/flight.git',
       'https://github.com/twitter/typeahead.js.git',
       'https://github.com/h5bp/html5-boilerplate.git',
@@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do
     ]
 
     # You can specify how many projects you need during seed execution
-    size = if ENV['SIZE'].present?
-             ENV['SIZE'].to_i
-           else
-             8
-           end
-
+    size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
 
     project_urls.first(size).each_with_index do |url, i|
       group_path, project_path = url.split('/')[-2..-1]
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 3e8cdcd67b4a66ab88eb6927453d98782b85bfa2..9739a5ac8d5a91df32437e0fb91cb28e24a35bce 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -1,7 +1,7 @@
 Gitlab::Seeder.quiet do
   Group.all.each do |group|
     User.all.sample(4).each do |user|
-      if group.add_users([user.id], Gitlab::Access.values.sample)
+      if group.add_user(user, Gitlab::Access.values.sample).persisted?
         print '.'
       else
         print 'F'
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
deleted file mode 100644
index 6441a036e75edf0b69abf3c1f4cb999404c86c13..0000000000000000000000000000000000000000
--- a/db/fixtures/development/14_builds.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-class Gitlab::Seeder::Builds
-  STAGES = %w[build notify_build test notify_test deploy notify_deploy]
-  BUILDS = [
-    { name: 'build:linux', stage: 'build', status: :success },
-    { name: 'build:osx', stage: 'build', status: :success },
-    { name: 'slack post build', stage: 'notify_build', status: :success },
-    { name: 'rspec:linux', stage: 'test', status: :success },
-    { name: 'rspec:windows', stage: 'test', status: :success },
-    { name: 'rspec:windows', stage: 'test', status: :success },
-    { name: 'rspec:osx', stage: 'test', status_event: :success },
-    { name: 'spinach:linux', stage: 'test', status: :pending },
-    { name: 'spinach:osx', stage: 'test', status: :canceled },
-    { name: 'cucumber:linux', stage: 'test', status: :running },
-    { name: 'cucumber:osx', stage: 'test', status: :failed },
-    { name: 'slack post test', stage: 'notify_test', status: :success },
-    { name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
-    { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success },
-  ]
-
-  def initialize(project)
-    @project = project
-  end
-
-  def seed!
-    pipelines.each do |pipeline|
-      begin
-        BUILDS.each { |opts| build_create!(pipeline, opts) }
-        commit_status_create!(pipeline, name: 'jenkins', status: :success)
-
-        print '.'
-      rescue ActiveRecord::RecordInvalid
-        print 'F'
-      end
-    end
-  end
-
-  def pipelines
-    commits = @project.repository.commits('master', limit: 5)
-    commits_sha = commits.map { |commit| commit.raw.id }
-    commits_sha.map do |sha|
-      @project.ensure_pipeline(sha, 'master')
-    end
-  rescue
-    []
-  end
-
-  def build_create!(pipeline, opts = {})
-    attributes = build_attributes_for(pipeline, opts)
-
-    Ci::Build.create!(attributes) do |build|
-      if opts[:name].start_with?('build')
-        artifacts_cache_file(artifacts_archive_path) do |file|
-          build.artifacts_file = file
-        end
-
-        artifacts_cache_file(artifacts_metadata_path) do |file|
-          build.artifacts_metadata = file
-        end
-      end
-
-      if %w(running success failed).include?(build.status)
-        # We need to set build trace after saving a build (id required)
-        build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
-      end
-    end
-  end
-
-  def commit_status_create!(pipeline, opts = {})
-    attributes = commit_status_attributes_for(pipeline, opts)
-    GenericCommitStatus.create!(attributes)
-  end
-
-  def commit_status_attributes_for(pipeline, opts)
-    { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
-      ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
-      created_at: Time.now, updated_at: Time.now
-    }.merge(opts)
-  end
-
-  def build_attributes_for(pipeline, opts)
-    commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
-  end
-
-  def build_user
-    @project.team.users.sample
-  end
-
-  def build_status
-    Ci::Build::AVAILABLE_STATUSES.sample
-  end
-
-  def stage_index(stage)
-    STAGES.index(stage) || 0
-  end
-
-  def artifacts_archive_path
-    Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
-  end
-
-  def artifacts_metadata_path
-    Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
-  end
-
-  def artifacts_cache_file(file_path)
-    cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
-
-    FileUtils.copy(file_path, cache_path)
-    File.open(cache_path) do |file|
-      yield file
-    end
-  end
-end
-
-Gitlab::Seeder.quiet do
-  Project.all.sample(10).each do |project|
-    project_builds = Gitlab::Seeder::Builds.new(project)
-    project_builds.seed!
-  end
-end
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..08ad3097d343a26852412ca475e1bd02f7306ae3
--- /dev/null
+++ b/db/fixtures/development/14_pipelines.rb
@@ -0,0 +1,158 @@
+class Gitlab::Seeder::Pipelines
+  STAGES = %w[build test deploy notify]
+  BUILDS = [
+    { name: 'build:linux', stage: 'build', status: :success },
+    { name: 'build:osx', stage: 'build', status: :success },
+    { name: 'rspec:linux 0 3', stage: 'test', status: :success },
+    { name: 'rspec:linux 1 3', stage: 'test', status: :success },
+    { name: 'rspec:linux 2 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 0 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 1 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+    { name: 'rspec:osx', stage: 'test', status_event: :success },
+    { name: 'spinach:linux', stage: 'test', status: :success },
+    { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
+    { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
+    { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
+    { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
+    { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
+    { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
+    { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
+    { name: 'slack', stage: 'notify', when: 'manual', status: :created },
+  ]
+
+  def initialize(project)
+    @project = project
+  end
+
+  def seed!
+    pipelines.each do |pipeline|
+      begin
+        BUILDS.each { |opts| build_create!(pipeline, opts) }
+        commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
+        print '.'
+      rescue ActiveRecord::RecordInvalid
+        print 'F'
+      ensure
+        pipeline.update_status
+      end
+    end
+  end
+
+  private
+
+  def pipelines
+    create_master_pipelines + create_merge_request_pipelines
+  end
+
+  def create_master_pipelines
+    @project.repository.commits('master', limit: 4).map do |commit|
+      create_pipeline!(@project, 'master', commit)
+    end
+  rescue
+    []
+  end
+
+  def create_merge_request_pipelines
+    pipelines = @project.merge_requests.first(3).map do |merge_request|
+      project = merge_request.source_project
+      branch = merge_request.source_branch
+
+      merge_request.commits.last(4).map do |commit|
+        create_pipeline!(project, branch, commit)
+      end
+    end
+
+    pipelines.flatten
+  rescue
+    []
+  end
+
+
+  def create_pipeline!(project, ref, commit)
+    project.pipelines.create(sha: commit.id, ref: ref)
+  end
+
+  def build_create!(pipeline, opts = {})
+    attributes = job_attributes(pipeline, opts)
+      .merge(commands: '$ build command')
+
+    Ci::Build.create!(attributes).tap do |build|
+      # We need to set build trace and artifacts after saving a build
+      # (id required), that is why we need `#tap` method instead of passing
+      # block directly to `Ci::Build#create!`.
+
+      setup_artifacts(build)
+      setup_build_log(build)
+      build.save
+    end
+  end
+
+  def setup_artifacts(build)
+    return unless %w[build test].include?(build.stage)
+
+    artifacts_cache_file(artifacts_archive_path) do |file|
+      build.artifacts_file = file
+    end
+
+    artifacts_cache_file(artifacts_metadata_path) do |file|
+      build.artifacts_metadata = file
+    end
+  end
+
+  def setup_build_log(build)
+    if %w(running success failed).include?(build.status)
+      build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
+    end
+  end
+
+  def commit_status_create!(pipeline, opts = {})
+    attributes = job_attributes(pipeline, opts)
+
+    GenericCommitStatus.create!(attributes)
+  end
+
+  def job_attributes(pipeline, opts)
+    { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
+      ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
+      created_at: Time.now, updated_at: Time.now
+    }.merge(opts)
+  end
+
+  def build_user
+    @project.team.users.sample
+  end
+
+  def build_status
+    Ci::Build::AVAILABLE_STATUSES.sample
+  end
+
+  def stage_index(stage)
+    STAGES.index(stage) || 0
+  end
+
+  def artifacts_archive_path
+    Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+  end
+
+  def artifacts_metadata_path
+    Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+  end
+
+  def artifacts_cache_file(file_path)
+    cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
+
+    FileUtils.copy(file_path, cache_path)
+    File.open(cache_path) do |file|
+      yield file
+    end
+  end
+end
+
+Gitlab::Seeder.quiet do
+  Project.all.sample(5).each do |project|
+    project_builds = Gitlab::Seeder::Pipelines.new(project)
+    project_builds.seed!
+  end
+end
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e882a492757053130531f959fcf4b6669bff5e36
--- /dev/null
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -0,0 +1,246 @@
+require 'sidekiq/testing'
+require './spec/support/test_env'
+
+class Gitlab::Seeder::CycleAnalytics
+  def initialize(project, perf: false)
+    @project = project
+    @user = User.order(:id).last
+    @issue_count = perf ? 1000 : 5
+    stub_git_pre_receive!
+  end
+
+  # The GitLab API needn't be running for the fixtures to be
+  # created. Since we're performing a number of git actions
+  # here (like creating a branch or committing a file), we need
+  # to disable the `pre_receive` hook in order to remove this
+  # dependency on the GitLab API.
+  def stub_git_pre_receive!
+    GitHooksService.class_eval do
+      def run_hook(name)
+        [true, '']
+      end
+    end
+  end
+
+  def seed_metrics!
+    @issue_count.times do |index|
+      # Issue
+      Timecop.travel 5.days.from_now
+      title = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+      issue = Issue.create(project: @project, title: title, author: @user)
+      issue_metrics = issue.metrics
+
+      # Milestones / Labels
+      Timecop.travel 5.days.from_now
+      if index.even?
+        issue_metrics.first_associated_with_milestone_at = rand(6..12).hours.from_now
+      else
+        issue_metrics.first_added_to_board_at = rand(6..12).hours.from_now
+      end
+
+      # Commit
+      Timecop.travel 5.days.from_now
+      issue_metrics.first_mentioned_in_commit_at = rand(6..12).hours.from_now
+
+      # MR
+      Timecop.travel 5.days.from_now
+      branch_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+      @project.repository.add_branch(@user, branch_name, 'master')
+      merge_request = MergeRequest.create(target_project: @project, source_project: @project, source_branch: branch_name, target_branch: 'master', title: branch_name, author: @user)
+      merge_request_metrics = merge_request.metrics
+
+      # MR closing issues
+      Timecop.travel 5.days.from_now
+      MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+
+      # Merge
+      Timecop.travel 5.days.from_now
+      merge_request_metrics.merged_at = rand(6..12).hours.from_now
+
+      # Start build
+      Timecop.travel 5.days.from_now
+      merge_request_metrics.latest_build_started_at = rand(6..12).hours.from_now
+
+      # Finish build
+      Timecop.travel 5.days.from_now
+      merge_request_metrics.latest_build_finished_at = rand(6..12).hours.from_now
+
+      # Deploy to production
+      Timecop.travel 5.days.from_now
+      merge_request_metrics.first_deployed_to_production_at = rand(6..12).hours.from_now
+
+      issue_metrics.save!
+      merge_request_metrics.save!
+
+      print '.'
+    end
+  end
+
+  def seed!
+    Sidekiq::Testing.inline! do
+      issues = create_issues
+      puts '.'
+
+      # Stage 1
+      Timecop.travel 5.days.from_now
+      add_milestones_and_list_labels(issues)
+      print '.'
+
+      # Stage 2
+      Timecop.travel 5.days.from_now
+      branches = mention_in_commits(issues)
+      print '.'
+
+      # Stage 3
+      Timecop.travel 5.days.from_now
+      merge_requests = create_merge_requests_closing_issues(issues, branches)
+      print '.'
+
+      # Stage 4
+      Timecop.travel 5.days.from_now
+      run_builds(merge_requests)
+      print '.'
+
+      # Stage 5
+      Timecop.travel 5.days.from_now
+      merge_merge_requests(merge_requests)
+      print '.'
+
+      # Stage 6 / 7
+      Timecop.travel 5.days.from_now
+      deploy_to_production(merge_requests)
+      print '.'
+    end
+
+    print '.'
+  end
+
+  private
+
+  def create_issues
+    Array.new(@issue_count) do
+      issue_params = {
+        title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
+        description: FFaker::Lorem.sentence,
+        state: 'opened',
+        assignee: @project.team.users.sample
+      }
+
+      Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
+    end
+  end
+
+  def add_milestones_and_list_labels(issues)
+    issues.shuffle.map.with_index do |issue, index|
+      Timecop.travel 12.hours.from_now
+
+      if index.even?
+        issue.update(milestone: @project.milestones.sample)
+      else
+        label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+        list_label = FactoryGirl.create(:label, title: label_name, project: issue.project)
+        FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label)
+        issue.update(labels: [list_label])
+      end
+
+      issue
+    end
+  end
+
+  def mention_in_commits(issues)
+    issues.map do |issue|
+      Timecop.travel 12.hours.from_now
+
+      branch_name = filename = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+
+      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)
+      issue.project.repository.commit(commit_sha)
+
+
+      GitPushService.new(issue.project,
+                         @user,
+                         oldrev: issue.project.repository.commit("master").sha,
+                         newrev: commit_sha,
+                         ref: 'refs/heads/master').execute
+
+      branch_name
+    end
+  end
+
+  def create_merge_requests_closing_issues(issues, branches)
+    issues.zip(branches).map do |issue, branch|
+      Timecop.travel 12.hours.from_now
+
+      opts = {
+        title: 'Cycle Analytics merge_request',
+        description: "Fixes #{issue.to_reference}",
+        source_branch: branch,
+        target_branch: 'master'
+      }
+
+      MergeRequests::CreateService.new(issue.project, @user, opts).execute
+    end
+  end
+
+  def run_builds(merge_requests)
+    merge_requests.each do |merge_request|
+      Timecop.travel 12.hours.from_now
+
+      service = Ci::CreatePipelineService.new(merge_request.project,
+                                              @user,
+                                              ref: "refs/heads/#{merge_request.source_branch}")
+      pipeline = service.execute(ignore_skip_ci: true, save_on_errors: false)
+
+      pipeline.run!
+      Timecop.travel rand(1..6).hours.from_now
+      pipeline.succeed!
+    end
+  end
+
+  def merge_merge_requests(merge_requests)
+    merge_requests.each do |merge_request|
+      Timecop.travel 12.hours.from_now
+
+      MergeRequests::MergeService.new(merge_request.project, @user).execute(merge_request)
+    end
+  end
+
+  def deploy_to_production(merge_requests)
+    merge_requests.each do |merge_request|
+      Timecop.travel 12.hours.from_now
+
+      CreateDeploymentService.new(merge_request.project, @user, {
+                                    environment: 'production',
+                                    ref: 'master',
+                                    tag: false,
+                                    sha: @project.repository.commit('master').sha
+                                  }).execute
+    end
+  end
+end
+
+Gitlab::Seeder.quiet do
+  if ENV['SEED_CYCLE_ANALYTICS']
+    Project.all.each do |project|
+      seeder = Gitlab::Seeder::CycleAnalytics.new(project)
+      seeder.seed!
+    end
+  elsif ENV['CYCLE_ANALYTICS_PERF_TEST']
+    seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+    seeder.seed!
+  elsif ENV['CYCLE_ANALYTICS_POPULATE_METRICS_DIRECTLY']
+    seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+    seeder.seed_metrics!
+  else
+    puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it."
+  end
+end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index 84463727b3b8a7cdf925594384c87361a488153c..e8de7ccf3db39dce3575d533a7fc1cf600510ae4 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,12 +1,15 @@
 # rubocop:disable all
 class MigrateRepoSize < ActiveRecord::Migration
+  DOWNTIME = false
+
   def up
     project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
 
     project_data.each do |project|
       id = project['id']
       namespace_path = project['namespace_path'] || ''
-      path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git')
+      repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+      path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
 
       begin
         repo = Gitlab::Git::Repository.new(path)
diff --git a/db/migrate/20160707104333_add_lock_to_issuables.rb b/db/migrate/20160707104333_add_lock_to_issuables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..54866d02cbc46a109e728ebe4c02f1852b440655
--- /dev/null
+++ b/db/migrate/20160707104333_add_lock_to_issuables.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 AddLockToIssuables < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    add_column :issues, :lock_version, :integer
+    add_column :merge_requests, :lock_version, :integer
+  end
+
+  def down
+    remove_column :issues, :lock_version
+    remove_column :merge_requests, :lock_version
+  end
+end
diff --git a/db/migrate/20160724205507_add_resolved_to_notes.rb b/db/migrate/20160724205507_add_resolved_to_notes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b8ebcdbd156019255d771a1097601d8b90a8698f
--- /dev/null
+++ b/db/migrate/20160724205507_add_resolved_to_notes.rb
@@ -0,0 +1,10 @@
+class AddResolvedToNotes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :notes, :resolved_at, :datetime
+    add_column :notes, :resolved_by_id, :integer
+  end
+end
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75a3eb15124cf68a235b4c34535b9857d1e3e814
--- /dev/null
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    constraint_name = 'merge_request_diffs_merge_request_id_key'
+
+    transaction do
+      if index_exists?(:merge_request_diffs, :merge_request_id)
+        remove_index(:merge_request_diffs, :merge_request_id)
+      end
+
+      # In some bizarre cases PostgreSQL might have a separate unique constraint
+      # that we'll need to drop.
+      if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+        execute("ALTER TABLE merge_request_diffs DROP CONSTRAINT IF EXISTS #{constraint_name};")
+      end
+    end
+  end
+
+  def down
+    unless index_exists?(:merge_request_diffs, :merge_request_id)
+      add_concurrent_index(:merge_request_diffs, :merge_request_id, unique: true)
+    end
+  end
+
+  def constraint_exists?(name)
+    indexes(:merge_request_diffs).map(&:name).include?(name)
+  end
+end
diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d04242dd25af8e410da13ec58d03e4023ef4f6a
--- /dev/null
+++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb
@@ -0,0 +1,17 @@
+class MergeRequestDiffAddIndex < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def up
+    add_concurrent_index :merge_request_diffs, :merge_request_id
+  end
+
+  def down
+    if index_exists?(:merge_request_diffs, :merge_request_id)
+      remove_index :merge_request_diffs, :merge_request_id
+    end
+  end
+end
diff --git a/db/migrate/20160727191041_create_boards.rb b/db/migrate/20160727191041_create_boards.rb
new file mode 100644
index 0000000000000000000000000000000000000000..56afbd4e030004fc0910add68f10b06d9ccaf0e1
--- /dev/null
+++ b/db/migrate/20160727191041_create_boards.rb
@@ -0,0 +1,13 @@
+class CreateBoards < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    create_table :boards do |t|
+      t.references :project, index: true, foreign_key: true, null: false
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20160727193336_create_lists.rb b/db/migrate/20160727193336_create_lists.rb
new file mode 100644
index 0000000000000000000000000000000000000000..61d501215f21472717867c75d38e23dab1507a6e
--- /dev/null
+++ b/db/migrate/20160727193336_create_lists.rb
@@ -0,0 +1,16 @@
+class CreateLists < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    create_table :lists do |t|
+      t.references :board, index: true, foreign_key: true, null: false
+      t.references :label, index: true, foreign_key: true
+      t.integer :list_type, null: false, default: 1
+      t.integer :position
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8db0fc60c4b687f0730cb4fa154c4691b9c14358
--- /dev/null
+++ b/db/migrate/20160801163421_add_expires_at_to_member.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 AddExpiresAtToMember < 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 change
+    add_column :members, :expires_at, :date
+  end
+end
diff --git a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..baf2e70b12769dcd2e07d4b0fb9ca0c1ba7f9c2d
--- /dev/null
+++ b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
@@ -0,0 +1,15 @@
+class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :lists, [:board_id, :label_id], unique: true
+  end
+
+  def down
+    remove_index :lists, column: [:board_id, :label_id] if index_exists?(:lists, [:board_id, :label_id], unique: true)
+  end
+end
diff --git a/db/migrate/20160808085531_add_token_to_build.rb b/db/migrate/20160808085531_add_token_to_build.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ed2a103ae3ca31d8ee9df38e215198e1bc5a28f
--- /dev/null
+++ b/db/migrate/20160808085531_add_token_to_build.rb
@@ -0,0 +1,10 @@
+class AddTokenToBuild < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    add_column :ci_builds, :token, :string
+  end
+end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10ef42afce18c316cb54296039c8b792dae65c62
--- /dev/null
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -0,0 +1,12 @@
+class AddIndexForBuildToken < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_index :ci_builds, :token, unique: true
+  end
+end
diff --git a/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7152bd04331a9ee29716a2dd85cfd993d23c5c67
--- /dev/null
+++ b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.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 AddColumnNameToU2fRegistrations < 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 change
+    add_column :u2f_registrations, :name, :string
+  end
+end
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..915d3d78e40ebe9ed09c616a0e4fd605f9fbc850
--- /dev/null
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -0,0 +1,10 @@
+class AddKodingToApplicationSettings < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :application_settings, :koding_enabled, :boolean
+    add_column :application_settings, :koding_url, :string
+  end
+end
diff --git a/db/migrate/20160817154936_add_discussion_ids_to_notes.rb b/db/migrate/20160817154936_add_discussion_ids_to_notes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..61facce665a4e3f9fe114fa8bb721cf547d2a3e4
--- /dev/null
+++ b/db/migrate/20160817154936_add_discussion_ids_to_notes.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDiscussionIdsToNotes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :notes, :discussion_id, :string
+    add_column :notes, :original_discussion_id, :string
+  end
+end
diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0ed538b0df8303cf67e760b24b92f0f6ce537330
--- /dev/null
+++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.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 AddExpiresAtToProjectGroupLinks < 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 change
+    add_column :project_group_links, :expires_at, :date
+  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
new file mode 100644
index 0000000000000000000000000000000000000000..b6e8bb18e7bf098c17d015323e37e4031457de2b
--- /dev/null
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToNoteDiscussionId < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_index :notes, :discussion_id
+  end
+end
diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c68cf01900f6a76b625fc0e1a324efe8d398d57
--- /dev/null
+++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'"
+  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
new file mode 100644
index 0000000000000000000000000000000000000000..f2cf956adc96faa27c3e52891c1ac9f7a2d6113a
--- /dev/null
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_column :users, :incoming_email_token, :string
+    add_concurrent_index :users, :incoming_email_token
+  end
+end
diff --git a/db/migrate/20160823081327_change_merge_error_to_text.rb b/db/migrate/20160823081327_change_merge_error_to_text.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7920389cd83a1eab517fa21a8f0c3d633ba84913
--- /dev/null
+++ b/db/migrate/20160823081327_change_merge_error_to_text.rb
@@ -0,0 +1,10 @@
+class ChangeMergeErrorToText < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration requires downtime because it alters a column from varchar(255) to text.'
+
+  def change
+    change_column :merge_requests, :merge_error, :text, limit: 65535
+  end
+end
diff --git a/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c169084e976977171b21bf994dc0330558ea1c87
--- /dev/null
+++ b/db/migrate/20160823213309_add_lfs_enabled_to_projects.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 AddLfsEnabledToProjects < 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 change
+    add_column :projects, :lfs_enabled, :boolean
+  end
+end
diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..65cf46308d95b73ffd7cedd709de650b22791e38
--- /dev/null
+++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb
@@ -0,0 +1,11 @@
+class DropUnusedCiTables < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    drop_table(:ci_services)
+    drop_table(:ci_web_hooks)
+  end
+end
diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9bb79b3c628f10f3b86ac9ea58d8451f00f8424
--- /dev/null
+++ b/db/migrate/20160824124900_add_table_issue_metrics.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 AddTableIssueMetrics < 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 = 'Adding foreign key'
+
+  # 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
+    create_table :issue_metrics do |t|
+      t.references :issue, index: { name: "index_issue_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+      t.datetime 'first_mentioned_in_commit_at'
+      t.datetime 'first_associated_with_milestone_at'
+      t.datetime 'first_added_to_board_at'
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e01cc5038b900efa9cfee7b6421750e1eeb11859
--- /dev/null
+++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableMergeRequestMetrics < 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 = 'Adding foreign key'
+
+  # 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
+    create_table :merge_request_metrics do |t|
+      t.references :merge_request, index: { name: "index_merge_request_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+      t.datetime 'latest_build_started_at'
+      t.datetime 'latest_build_finished_at'
+      t.datetime 'first_deployed_to_production_at', index: true
+      t.datetime 'merged_at'
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c55bc23cf2fecf8f419d26594d95b127bfbc183
--- /dev/null
+++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
@@ -0,0 +1,16 @@
+class EnsureLockVersionHasNoDefault < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_default :issues, :lock_version, nil
+    change_column_default :merge_requests, :lock_version, nil
+
+    execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0')
+    execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0')
+  end
+
+  def down
+  end
+end
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8753e55e058b2ba077d2c6f525b85e7969540fb2
--- /dev/null
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddMarkdownCacheColumns < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  COLUMNS = {
+    abuse_reports: [:message],
+    appearances: [:description],
+    application_settings: [
+      :sign_in_text,
+      :help_page_text,
+      :shared_runners_text,
+      :after_sign_up_text
+    ],
+    broadcast_messages: [:message],
+    issues: [:title, :description],
+    labels: [:description],
+    merge_requests: [:title, :description],
+    milestones: [:title, :description],
+    namespaces: [:description],
+    notes: [:note],
+    projects: [:description],
+    releases: [:description],
+    snippets: [:title, :content],
+  }
+
+  def change
+    COLUMNS.each do |table, columns|
+      columns.each do |column|
+        add_column table, "#{column}_html", :text
+      end
+    end
+  end
+end
diff --git a/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a27947212f6031b07086c59fe6f9d408bf6bcb27
--- /dev/null
+++ b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToWebHooks < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default :web_hooks, :confidential_issues_events, :boolean, default: false, allow_null: false
+  end
+
+  def down
+    remove_column :web_hooks, :confidential_issues_events
+  end
+end
diff --git a/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
new file mode 100644
index 0000000000000000000000000000000000000000..030e7c39350e3fdc05f508d008afa3b5b50d375e
--- /dev/null
+++ b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToServices < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default :services, :confidential_issues_events, :boolean, default: true, allow_null: false
+  end
+
+  def down
+    remove_column :services, :confidential_issues_events
+  end
+end
diff --git a/db/migrate/20160830232601_change_lock_version_not_null.rb b/db/migrate/20160830232601_change_lock_version_not_null.rb
new file mode 100644
index 0000000000000000000000000000000000000000..01c58ed5bdca892342b4b8eaefb2c4133c7ae1c0
--- /dev/null
+++ b/db/migrate/20160830232601_change_lock_version_not_null.rb
@@ -0,0 +1,13 @@
+class ChangeLockVersionNotNull < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_null :issues, :lock_version, true
+    change_column_null :merge_requests, :lock_version, true
+  end
+
+  def down
+  end
+end
diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d76a015a08615f3044c988dfde670463036180a
--- /dev/null
+++ b/db/migrate/20160831214002_create_project_features.rb
@@ -0,0 +1,16 @@
+class CreateProjectFeatures < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def change
+    create_table :project_features do |t|
+      t.belongs_to :project, index: true
+      t.integer  :merge_requests_access_level
+      t.integer  :issues_access_level
+      t.integer  :wiki_access_level
+      t.integer  :snippets_access_level
+      t.integer  :builds_access_level
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93f9821bc76e21fc743d401eb892358884cc0078
--- /dev/null
+++ b/db/migrate/20160831214543_migrate_project_features.rb
@@ -0,0 +1,44 @@
+class MigrateProjectFeatures < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON =
+    <<-EOT
+      Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
+      a new table called project_features.
+    EOT
+
+  def up
+    sql =
+      %Q{
+        INSERT INTO project_features(project_id, issues_access_level, merge_requests_access_level, wiki_access_level,
+        builds_access_level, snippets_access_level, created_at, updated_at)
+          SELECT
+          id AS project_id,
+          CASE WHEN issues_enabled IS true THEN 20 ELSE 0 END AS issues_access_level,
+          CASE WHEN merge_requests_enabled IS true THEN 20 ELSE 0 END AS merge_requests_access_level,
+          CASE WHEN wiki_enabled IS true THEN 20 ELSE 0 END AS wiki_access_level,
+          CASE WHEN builds_enabled IS true THEN 20 ELSE 0 END AS builds_access_level,
+          CASE WHEN snippets_enabled IS true THEN 20 ELSE 0 END AS snippets_access_level,
+          created_at,
+          updated_at
+          FROM projects
+      }
+
+    execute(sql)
+  end
+
+  def down
+    sql = %Q{
+      UPDATE projects
+      SET
+      issues_enabled = COALESCE((SELECT CASE WHEN issues_access_level = 20 THEN true ELSE false END AS issues_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+      merge_requests_enabled = COALESCE((SELECT CASE WHEN merge_requests_access_level = 20 THEN true ELSE false END AS merge_requests_enabled FROM project_features WHERE project_features.project_id = projects.id),true),
+      wiki_enabled = COALESCE((SELECT CASE WHEN wiki_access_level = 20 THEN true ELSE false END AS wiki_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+      builds_enabled = COALESCE((SELECT CASE WHEN builds_access_level = 20 THEN true ELSE false END AS builds_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
+      snippets_enabled = COALESCE((SELECT CASE WHEN snippets_access_level = 20 THEN true ELSE false END AS snippets_enabled FROM project_features WHERE project_features.project_id = projects.id),true)
+    }
+
+    execute(sql)
+  end
+end
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a2c207b49ea7ee9d957add561f46594c15a5c677
--- /dev/null
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.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 RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = true
+  DOWNTIME_REASON = "Removing fields from database requires downtine."
+
+  def up
+    remove_column :projects, :issues_enabled
+    remove_column :projects, :merge_requests_enabled
+    remove_column :projects, :builds_enabled
+    remove_column :projects, :wiki_enabled
+    remove_column :projects, :snippets_enabled
+  end
+
+  # Ugly SQL but the only way i found to make it work on both Postgres and Mysql
+  # It will be slow but it is ok since it is a revert method
+  def down
+    add_column_with_default(:projects, :issues_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:projects, :merge_requests_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:projects, :builds_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:projects, :wiki_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:projects, :snippets_enabled, :boolean, default: true, allow_null: false)
+  end
+end
diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1a1f001cb303d7908cf99fc40bbbcdfca5f1b9c
--- /dev/null
+++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
@@ -0,0 +1,15 @@
+class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    update_column_in_batches(:web_hooks, :confidential_issues_events, true) do |table, query|
+      query.where(table[:issues_events].eq(true))
+    end
+  end
+
+  def down
+    # noop
+  end
+end
diff --git a/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd413d1ca8cf20147d319f4ca1a45814b4d97bd5
--- /dev/null
+++ b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToNamespaces < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :namespaces, :lfs_enabled, :boolean
+  end
+end
diff --git a/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a80a57254dd6f4a0c87ef9b17bee79590409f05d
--- /dev/null
+++ b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
@@ -0,0 +1,39 @@
+class DropGitoriousFieldFromApplicationSettings < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # After the deploy the caches will be cold anyway
+  DOWNTIME = false
+
+  def up
+    require 'yaml'
+
+    import_sources = connection.execute('SELECT import_sources FROM application_settings;')
+    return unless import_sources.first # support empty databases
+
+    yaml = if Gitlab::Database.postgresql?
+             import_sources.values[0][0]
+           else
+             import_sources.first[0]
+           end
+
+    yaml = YAML.safe_load(yaml)
+    yaml.delete 'gitorious'
+
+    # No need for a WHERE clause as there is only one
+    connection.execute("UPDATE application_settings SET import_sources = #{update_yaml(yaml)}")
+  end
+
+  def down
+    # noop, gitorious still yields a 404 anyway
+  end
+
+  private
+
+  def connection
+    ActiveRecord::Base.connection
+  end
+
+  def update_yaml(yaml)
+    connection.quote(YAML.dump(yaml))
+  end
+end
diff --git a/db/migrate/20160907131111_add_environment_type_to_environments.rb b/db/migrate/20160907131111_add_environment_type_to_environments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fac73753d5b44fba877c479b121b8ed1889adaf0
--- /dev/null
+++ b/db/migrate/20160907131111_add_environment_type_to_environments.rb
@@ -0,0 +1,9 @@
+class AddEnvironmentTypeToEnvironments < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :environments, :environment_type, :string
+  end
+end
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
new file mode 100644
index 0000000000000000000000000000000000000000..18ea9d43a43d0d0fe6f8f2612f88daaec9eb1841
--- /dev/null
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.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 RemoveProjectsPushesSinceGc < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration removes an existing column'
+
+  disable_ddl_transaction!
+
+  def up
+    remove_column :projects, :pushes_since_gc
+  end
+
+  def down
+    add_column_with_default :projects, :pushes_since_gc, :integer, default: 0
+  end
+end
diff --git a/db/migrate/20160913212128_change_artifacts_size_column.rb b/db/migrate/20160913212128_change_artifacts_size_column.rb
new file mode 100644
index 0000000000000000000000000000000000000000..063bbca537c04d70573e87f36d9a7cdfcb9146a6
--- /dev/null
+++ b/db/migrate/20160913212128_change_artifacts_size_column.rb
@@ -0,0 +1,15 @@
+class ChangeArtifactsSizeColumn < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+
+  DOWNTIME_REASON = 'Changing an integer column size requires a full table rewrite.'
+
+  def up
+    change_column :ci_builds, :artifacts_size, :integer, limit: 8
+  end
+
+  def down
+    # do nothing
+  end
+end
diff --git a/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fad62d716b300f555467428a6500e598479b95fe
--- /dev/null
+++ b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
@@ -0,0 +1,17 @@
+class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default(:projects,
+                            :only_allow_merge_if_all_discussions_are_resolved,
+                            :boolean,
+                            default: false)
+  end
+
+  def down
+    remove_column(:projects, :only_allow_merge_if_all_discussions_are_resolved)
+  end
+end
diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94874a853dae6e15df78dd78a7ebfbb10a3be4ed
--- /dev/null
+++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateMergeRequestsClosingIssues < 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 = 'Adding foreign keys'
+
+  # 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
+    create_table :merge_requests_closing_issues do |t|
+      t.references :merge_request, foreign_key: { on_delete: :cascade }, index: true, null: false
+      t.references :issue, foreign_key: { on_delete: :cascade }, index: true, null: false
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66172bda6ffc92f967ea0f0d52289a60916bf754
--- /dev/null
+++ b/db/migrate/20160919144305_add_type_to_labels.rb
@@ -0,0 +1,14 @@
+class AddTypeToLabels < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.'
+
+  def change
+    add_column :labels, :type, :string
+
+    update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
+      query.where(table[:project_id].not_eq(nil))
+    end
+  end
+end
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..05e21af058447a05024abb59c69390bf66ef085a
--- /dev/null
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -0,0 +1,13 @@
+class AddGroupIdToLabels < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_column :labels, :group_id, :integer
+    add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
+    add_concurrent_index :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
new file mode 100644
index 0000000000000000000000000000000000000000..b5de552b98cfb3e76e410f6bd089bdd65837fe82
--- /dev/null
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -0,0 +1,11 @@
+class AddIndexToLabelsTitle < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_index :labels, :title
+  end
+end
diff --git a/db/migrate/20160926145521_add_organization_to_user.rb b/db/migrate/20160926145521_add_organization_to_user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0bef6e7548b42d6bfb61f0cbb7c15b1edb0cd54
--- /dev/null
+++ b/db/migrate/20160926145521_add_organization_to_user.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddOrganizationToUser < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :users, :organization, :string
+  end
+end
diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ccb546654f988220a834278e8c56784fa3dec924
--- /dev/null
+++ b/db/migrate/20161006104309_add_state_to_environment.rb
@@ -0,0 +1,15 @@
+class AddStateToEnvironment < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    add_column_with_default(:environments, :state, :string, default: :available)
+  end
+
+  def down
+    remove_column(:environments, :state)
+  end
+end
diff --git a/db/migrate/20161007133303_precalculate_trending_projects.rb b/db/migrate/20161007133303_precalculate_trending_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b324cd942689a77d4b93de461bfbff51696a9dab
--- /dev/null
+++ b/db/migrate/20161007133303_precalculate_trending_projects.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PrecalculateTrendingProjects < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    create_table :trending_projects do |t|
+      t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
+    end
+
+    timestamp = connection.quote(1.month.ago)
+
+    # We're hardcoding the visibility level (public) here so that if it ever
+    # changes this query doesn't suddenly use the new value (which may break
+    # later migrations).
+    visibility = 20
+
+    execute <<-EOF.strip_heredoc
+      INSERT INTO trending_projects (project_id)
+      SELECT project_id
+      FROM notes
+      INNER JOIN projects ON projects.id = notes.project_id
+      WHERE notes.created_at >= #{timestamp}
+      AND notes.system IS FALSE
+      AND projects.visibility_level = #{visibility}
+      GROUP BY project_id
+      ORDER BY count(*) DESC
+      LIMIT 100;
+    EOF
+  end
+
+  def down
+    drop_table :trending_projects
+  end
+end
diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
new file mode 100644
index 0000000000000000000000000000000000000000..319d86ac1591ed10baa3f4a5f70c80380ed31f86
--- /dev/null
+++ b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
@@ -0,0 +1,10 @@
+class RemoveInactiveJiraServiceProperties < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = "Removes all inactive jira_service properties"
+
+  def up
+    execute("UPDATE services SET properties = '{}' WHERE services.type = 'JiraService' and services.active = false")
+  end
+end
diff --git a/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb b/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b33da3ea111e7938fd23f0a692ff23e91570e36
--- /dev/null
+++ b/db/migrate/20161012180455_add_repository_access_level_to_project_feature.rb
@@ -0,0 +1,14 @@
+class AddRepositoryAccessLevelToProjectFeature < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    add_column_with_default(:project_features, :repository_access_level, :integer, default: ProjectFeature::ENABLED)
+  end
+
+  def down
+    remove_column :project_features, :repository_access_level
+  end
+end
diff --git a/db/migrate/20161014173530_create_label_priorities.rb b/db/migrate/20161014173530_create_label_priorities.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c22841c28a27af37d73a525033d80e5ec82aa84
--- /dev/null
+++ b/db/migrate/20161014173530_create_label_priorities.rb
@@ -0,0 +1,25 @@
+class CreateLabelPriorities < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration adds foreign keys'
+
+  disable_ddl_transaction!
+
+  def up
+    create_table :label_priorities do |t|
+      t.references :project, foreign_key: { on_delete: :cascade }, null: false
+      t.references :label, foreign_key: { on_delete: :cascade }, null: false
+      t.integer :priority, null: false
+
+      t.timestamps null: false
+    end
+
+    add_concurrent_index :label_priorities, [:project_id, :label_id], unique: true
+    add_concurrent_index :label_priorities, :priority
+  end
+
+  def down
+    drop_table :label_priorities
+  end
+end
diff --git a/db/migrate/20161017095000_add_properties_to_deployment.rb b/db/migrate/20161017095000_add_properties_to_deployment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f620ee0de1cd76c639d6289605b70c33772988ae
--- /dev/null
+++ b/db/migrate/20161017095000_add_properties_to_deployment.rb
@@ -0,0 +1,9 @@
+class AddPropertiesToDeployment < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :deployments, :on_stop, :string
+  end
+end
diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2b56ebfb7b5ec12ec465790aaede27c3372ab9f
--- /dev/null
+++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb
@@ -0,0 +1,32 @@
+class AddUniqueIndexToLabels < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration removes duplicated labels.'
+
+  disable_ddl_transaction!
+
+  def up
+    select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
+      label_title = quote_string(label['title'])
+      duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
+      label_id = duplicated_ids.first
+      duplicated_ids.delete(label_id)
+
+      execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})")
+      execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})")
+    end
+
+    remove_index :labels, column: :project_id if index_exists?(:labels, :project_id)
+    remove_index :labels, column: :title if index_exists?(:labels, :title)
+
+    add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true
+  end
+
+  def down
+    remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true)
+
+    add_concurrent_index :labels, :project_id
+    add_concurrent_index :labels, :title
+  end
+end
diff --git a/db/migrate/20161018024215_migrate_labels_priority.rb b/db/migrate/20161018024215_migrate_labels_priority.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22bec2382f46025f90f53fff50c634f8ccf33a05
--- /dev/null
+++ b/db/migrate/20161018024215_migrate_labels_priority.rb
@@ -0,0 +1,36 @@
+class MigrateLabelsPriority < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.'
+
+  disable_ddl_transaction!
+
+  def up
+    execute <<-EOF.strip_heredoc
+      INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at)
+      SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW()
+      FROM labels
+      WHERE labels.project_id IS NOT NULL
+        AND labels.priority IS NOT NULL;
+    EOF
+  end
+
+  def down
+    if Gitlab::Database.mysql?
+      execute <<-EOF.strip_heredoc
+        UPDATE labels
+          INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id
+        SET labels.priority = label_priorities.priority;
+      EOF
+    else
+      execute <<-EOF.strip_heredoc
+        UPDATE labels
+        SET priority = label_priorities.priority
+        FROM label_priorities
+        WHERE labels.id = label_priorities.label_id
+          AND labels.project_id = label_priorities.project_id;
+      EOF
+    end
+  end
+end
diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7416cca6646432207bca38927097412b5136ce5
--- /dev/null
+++ b/db/migrate/20161018024550_remove_priority_from_labels.rb
@@ -0,0 +1,17 @@
+class RemovePriorityFromLabels < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'This migration removes an existing column'
+
+  disable_ddl_transaction!
+
+  def up
+    remove_column :labels, :priority, :integer, index: true
+  end
+
+  def down
+    add_column :labels, :priority, :integer
+    add_concurrent_index :labels, :priority
+  end
+end
diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a576bb7b6222920b939b928a543835b7099a63b8
--- /dev/null
+++ b/db/migrate/20161018124658_make_project_owners_masters.rb
@@ -0,0 +1,15 @@
+class MakeProjectOwnersMasters < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    update_column_in_batches(:members, :access_level, 40) do |table, query|
+      query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project')))
+    end
+  end
+
+  def down
+    # do nothing
+  end
+end
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e875213ab96db659ef130a0f60f4e0f03dfc8957
--- /dev/null
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -0,0 +1,109 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+
+  DOWNTIME_REASON = <<-EOF
+  Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+  Sidekiq will result in the loss of jobs that are scheduled after this
+  migration completes.
+  EOF
+
+  disable_ddl_transaction!
+
+  # Jobs for which the queue names have been changed (e.g. multiple workers
+  # using the same non-default queue).
+  #
+  # The keys are the old queue names, the values the jobs to move and their new
+  # queue names.
+  RENAMED_QUEUES = {
+    gitlab_shell: {
+      'GitGarbageCollectorWorker' => :git_garbage_collector,
+      'ProjectExportWorker'       => :project_export,
+      'RepositoryForkWorker'      => :repository_fork,
+      'RepositoryImportWorker'    => :repository_import
+    },
+    project_web_hook: {
+      'ProjectServiceWorker' => :project_service
+    },
+    incoming_email: {
+      'EmailReceiverWorker' => :email_receiver
+    },
+    mailers: {
+      'EmailsOnPushWorker' => :emails_on_push
+    },
+    default: {
+      'AdminEmailWorker'                        => :cronjob,
+      'BuildCoverageWorker'                     => :build,
+      'BuildEmailWorker'                        => :build,
+      'BuildFinishedWorker'                     => :build,
+      'BuildHooksWorker'                        => :build,
+      'BuildSuccessWorker'                      => :build,
+      'ClearDatabaseCacheWorker'                => :clear_database_cache,
+      'DeleteUserWorker'                        => :delete_user,
+      'ExpireBuildArtifactsWorker'              => :cronjob,
+      'ExpireBuildInstanceArtifactsWorker'      => :expire_build_instance_artifacts,
+      'GroupDestroyWorker'                      => :group_destroy,
+      'ImportExportProjectCleanupWorker'        => :cronjob,
+      'IrkerWorker'                             => :irker,
+      'MergeWorker'                             => :merge,
+      'NewNoteWorker'                           => :new_note,
+      'PipelineHooksWorker'                     => :pipeline,
+      'PipelineMetricsWorker'                   => :pipeline,
+      'PipelineProcessWorker'                   => :pipeline,
+      'PipelineSuccessWorker'                   => :pipeline,
+      'PipelineUpdateWorker'                    => :pipeline,
+      'ProjectCacheWorker'                      => :project_cache,
+      'ProjectDestroyWorker'                    => :project_destroy,
+      'PruneOldEventsWorker'                    => :cronjob,
+      'RemoveExpiredGroupLinksWorker'           => :cronjob,
+      'RemoveExpiredMembersWorker'              => :cronjob,
+      'RepositoryArchiveCacheWorker'            => :cronjob,
+      'RepositoryCheck::BatchWorker'            => :cronjob,
+      'RepositoryCheck::ClearWorker'            => :repository_check,
+      'RepositoryCheck::SingleRepositoryWorker' => :repository_check,
+      'RequestsProfilesWorker'                  => :cronjob,
+      'StuckCiBuildsWorker'                     => :cronjob,
+      'UpdateMergeRequestsWorker'               => :update_merge_requests
+    }
+  }
+
+  def up
+    Sidekiq.redis do |redis|
+      RENAMED_QUEUES.each do |queue, jobs|
+        migrate_from_queue(redis, queue, jobs)
+      end
+    end
+  end
+
+  def down
+    Sidekiq.redis do |redis|
+      RENAMED_QUEUES.each do |dest_queue, jobs|
+        jobs.each do |worker, from_queue|
+          migrate_from_queue(redis, from_queue, worker => dest_queue)
+        end
+      end
+    end
+  end
+
+  def migrate_from_queue(redis, queue, job_mapping)
+    while job = redis.lpop("queue:#{queue}")
+      payload = JSON.load(job)
+      new_queue = job_mapping[payload['class']]
+
+      # If we have no target queue to migrate to we're probably dealing with
+      # some ancient job for which the worker no longer exists. In that case
+      # there's no sane option we can take, other than just dropping the job.
+      next unless new_queue
+
+      payload['queue'] = new_queue
+
+      redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+    end
+  end
+end
diff --git a/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4554e14b0df612cc0b9e09efc642a66913fbb9be
--- /dev/null
+++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
@@ -0,0 +1,28 @@
+class GenerateProjectFeatureForProjects < ActiveRecord::Migration
+  DOWNTIME = true
+
+  DOWNTIME_REASON = <<-HEREDOC
+    Application was eager loading project_feature for all projects generating an extra query
+    everytime a project was fetched. We removed that behavior to avoid the extra query, this migration
+    makes sure all projects have a project_feature record associated.
+  HEREDOC
+
+  def up
+    # Generate enabled values for each project feature 20, 20, 20, 20, 20
+    # All features are enabled by default
+    enabled_values = [ProjectFeature::ENABLED] * 5
+
+    execute <<-EOF.strip_heredoc
+      INSERT INTO project_features
+      (project_id, merge_requests_access_level, builds_access_level,
+      issues_access_level, snippets_access_level, wiki_access_level)
+      (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features
+      ON project_features.project_id = projects.id
+      WHERE project_features.id IS NULL)
+    EOF
+  end
+
+  def down
+    "Not needed"
+  end
+end
diff --git a/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b47f3aa281018e8407f1328b15d30366f18cec9e
--- /dev/null
+++ b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLockVersionToBuildAndPipelines < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    add_column :ci_builds, :lock_version, :integer
+    add_column :ci_commits, :lock_version, :integer
+  end
+end
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06d07bdb83516776da89b94ce2b4a012cabdc76b
--- /dev/null
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -0,0 +1,63 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+
+  DOWNTIME_REASON = <<-EOF
+  Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+  Sidekiq will result in the loss of jobs that are scheduled after this
+  migration completes.
+  EOF
+
+  disable_ddl_transaction!
+
+  # Jobs for which the queue names have been changed (e.g. multiple workers
+  # using the same non-default queue).
+  #
+  # The keys are the old queue names, the values the jobs to move and their new
+  # queue names.
+  RENAMED_QUEUES = {
+      incoming_email: {
+          'EmailReceiverWorker' => :email_receiver
+      }
+  }
+
+  def up
+    Sidekiq.redis do |redis|
+      RENAMED_QUEUES.each do |queue, jobs|
+        migrate_from_queue(redis, queue, jobs)
+      end
+    end
+  end
+
+  def down
+    Sidekiq.redis do |redis|
+      RENAMED_QUEUES.each do |dest_queue, jobs|
+        jobs.each do |worker, from_queue|
+          migrate_from_queue(redis, from_queue, worker => dest_queue)
+        end
+      end
+    end
+  end
+
+  def migrate_from_queue(redis, queue, job_mapping)
+    while job = redis.lpop("queue:#{queue}")
+      payload = JSON.load(job)
+      new_queue = job_mapping[payload['class']]
+
+      # If we have no target queue to migrate to we're probably dealing with
+      # some ancient job for which the worker no longer exists. In that case
+      # there's no sane option we can take, other than just dropping the job.
+      next unless new_queue
+
+      payload['queue'] = new_queue
+
+      redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+    end
+  end
+end
diff --git a/db/migrate/20161025231710_migrate_jira_to_gem.rb b/db/migrate/20161025231710_migrate_jira_to_gem.rb
new file mode 100644
index 0000000000000000000000000000000000000000..870b00411d2bf0fa11fa0cda43a3972e2ca879ea
--- /dev/null
+++ b/db/migrate/20161025231710_migrate_jira_to_gem.rb
@@ -0,0 +1,73 @@
+class MigrateJiraToGem < ActiveRecord::Migration
+  DOWNTIME = true
+
+  DOWNTIME_REASON = <<-HEREDOC
+    Refactor all Jira services properties(serialized field) to use new jira-ruby gem.
+    There were properties on old Jira service that are not needed anymore after the
+    service refactoring: api_url, project_url, new_issue_url, issues_url.
+    We extract the new necessary some properties from old keys and delete them:
+    taking project_key from project_url and url from api_url
+  HEREDOC
+
+  def up
+    active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true"
+
+    select_all(active_services_query).each do |service|
+      id = service['id']
+      properties = JSON.parse(service['properties'])
+      properties_was = properties.clone
+
+      # Migrate `project_url` to `project_key`
+      # Ignore if `project_url` doesn't have jql project query with project key
+      if properties['project_url'].present?
+        jql = properties['project_url'].match('project=([A-Za-z]*)')
+        properties['project_key'] = jql.captures.first if jql
+      end
+
+      # Migrate `api_url` to `url`
+      if properties['api_url'].present?
+        url = properties['api_url'].match('(.*)\/rest\/api')
+        properties['url'] = url.captures.first if url
+      end
+
+      # Delete now unnecessary properties
+      properties.delete('api_url')
+      properties.delete('project_url')
+      properties.delete('new_issue_url')
+      properties.delete('issues_url')
+
+      # Update changes properties
+      if properties != properties_was
+        execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
+      end
+    end
+  end
+
+  def down
+    active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true"
+
+    select_all(active_services_query).each do |service|
+      id = service['id']
+      properties = JSON.parse(service['properties'])
+      properties_was = properties.clone
+
+      # Rebuild old properties based on sane defaults
+      if properties['url'].present?
+        properties['api_url'] = "#{properties['url']}/rest/api/2"
+        properties['project_url'] =
+          "#{properties['url']}/issues/?jql=project=#{properties['project_key']}"
+        properties['issues_url'] = "#{properties['url']}/browse/:id"
+        properties['new_issue_url'] = "#{properties['url']}/secure/CreateIssue.jspa"
+      end
+
+      # Delete the new properties
+      properties.delete('url')
+      properties.delete('project_key')
+
+      # Update changes properties
+      if properties != properties_was
+        execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
+      end
+    end
+  end
+end
diff --git a/db/migrate/20161031155516_add_housekeeping_to_application_settings.rb b/db/migrate/20161031155516_add_housekeeping_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a451fb575b8c83441544b08786fbb2c263be6b7
--- /dev/null
+++ b/db/migrate/20161031155516_add_housekeeping_to_application_settings.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddHousekeepingToApplicationSettings < 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_column_with_default(:application_settings, :housekeeping_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:application_settings, :housekeeping_bitmaps_enabled, :boolean, default: true, allow_null: false)
+    add_column_with_default(:application_settings, :housekeeping_incremental_repack_period, :integer, default: 10, allow_null: false)
+    add_column_with_default(:application_settings, :housekeeping_full_repack_period, :integer, default: 50, allow_null: false)
+    add_column_with_default(:application_settings, :housekeeping_gc_period, :integer, default: 200, allow_null: false)
+  end
+
+  def down
+    remove_column(:application_settings, :housekeeping_enabled, :boolean, default: true, allow_null: false)
+    remove_column(:application_settings, :housekeeping_bitmaps_enabled, :boolean, default: true, allow_null: false)
+    remove_column(:application_settings, :housekeeping_incremental_repack_period, :integer, default: 10, allow_null: false)
+    remove_column(:application_settings, :housekeeping_full_repack_period, :integer, default: 50, allow_null: false)
+    remove_column(:application_settings, :housekeeping_gc_period, :integer, default: 200, allow_null: false)
+  end
+end
diff --git a/db/migrate/20161103171205_rename_repository_storage_column.rb b/db/migrate/20161103171205_rename_repository_storage_column.rb
new file mode 100644
index 0000000000000000000000000000000000000000..932805739394b592086b826be37ba0e68e424e8e
--- /dev/null
+++ b/db/migrate/20161103171205_rename_repository_storage_column.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 RenameRepositoryStorageColumn < 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 application_settings.repository_storage column'
+
+  # 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 :application_settings, :repository_storage, :repository_storages
+  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
new file mode 100644
index 0000000000000000000000000000000000000000..750a6a8c51ebab2aef1adc088a9593e5e43423dd
--- /dev/null
+++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb
@@ -0,0 +1,12 @@
+class AddProjectImportDataProjectIndex < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_index :project_import_data, :project_id
+  end
+end
diff --git a/lib/tasks/.gitkeep b/db/post_migrate/.gitkeep
similarity index 100%
rename from lib/tasks/.gitkeep
rename to db/post_migrate/.gitkeep
diff --git a/db/schema.rb b/db/schema.rb
index d8c6922fb1d7d91b7b8b7e115c834b5eb7090c85..47e468ad896d309de889a27008f2911c9b067e48 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,93 +11,106 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20160811172945) do
+ActiveRecord::Schema.define(version: 20161106185620) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
   enable_extension "pg_trgm"
 
   create_table "abuse_reports", force: :cascade do |t|
-    t.integer  "reporter_id"
-    t.integer  "user_id"
-    t.text     "message"
+    t.integer "reporter_id"
+    t.integer "user_id"
+    t.text "message"
     t.datetime "created_at"
     t.datetime "updated_at"
+    t.text "message_html"
   end
 
   create_table "appearances", force: :cascade do |t|
-    t.string   "title"
-    t.text     "description"
-    t.string   "header_logo"
-    t.string   "logo"
-    t.datetime "created_at",  null: false
-    t.datetime "updated_at",  null: false
+    t.string "title"
+    t.text "description"
+    t.string "header_logo"
+    t.string "logo"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.text "description_html"
   end
 
   create_table "application_settings", force: :cascade do |t|
-    t.integer  "default_projects_limit"
-    t.boolean  "signup_enabled"
-    t.boolean  "signin_enabled"
-    t.boolean  "gravatar_enabled"
-    t.text     "sign_in_text"
+    t.integer "default_projects_limit"
+    t.boolean "signup_enabled"
+    t.boolean "signin_enabled"
+    t.boolean "gravatar_enabled"
+    t.text "sign_in_text"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "home_page_url"
-    t.integer  "default_branch_protection",             default: 2
-    t.text     "restricted_visibility_levels"
-    t.boolean  "version_check_enabled",                 default: true
-    t.integer  "max_attachment_size",                   default: 10,          null: false
-    t.integer  "default_project_visibility"
-    t.integer  "default_snippet_visibility"
-    t.text     "domain_whitelist"
-    t.boolean  "user_oauth_applications",               default: true
-    t.string   "after_sign_out_path"
-    t.integer  "session_expire_delay",                  default: 10080,       null: false
-    t.text     "import_sources"
-    t.text     "help_page_text"
-    t.string   "admin_notification_email"
-    t.boolean  "shared_runners_enabled",                default: true,        null: false
-    t.integer  "max_artifacts_size",                    default: 100,         null: false
-    t.string   "runners_registration_token"
-    t.boolean  "require_two_factor_authentication",     default: false
-    t.integer  "two_factor_grace_period",               default: 48
-    t.boolean  "metrics_enabled",                       default: false
-    t.string   "metrics_host",                          default: "localhost"
-    t.integer  "metrics_pool_size",                     default: 16
-    t.integer  "metrics_timeout",                       default: 10
-    t.integer  "metrics_method_call_threshold",         default: 10
-    t.boolean  "recaptcha_enabled",                     default: false
-    t.string   "recaptcha_site_key"
-    t.string   "recaptcha_private_key"
-    t.integer  "metrics_port",                          default: 8089
-    t.boolean  "akismet_enabled",                       default: false
-    t.string   "akismet_api_key"
-    t.integer  "metrics_sample_interval",               default: 15
-    t.boolean  "sentry_enabled",                        default: false
-    t.string   "sentry_dsn"
-    t.boolean  "email_author_in_body",                  default: false
-    t.integer  "default_group_visibility"
-    t.boolean  "repository_checks_enabled",             default: false
-    t.text     "shared_runners_text"
-    t.integer  "metrics_packet_size",                   default: 1
-    t.text     "disabled_oauth_sign_in_sources"
-    t.string   "health_check_access_token"
-    t.boolean  "send_user_confirmation_email",          default: false
-    t.integer  "container_registry_token_expire_delay", default: 5
-    t.boolean  "user_default_external",                 default: false,       null: false
-    t.text     "after_sign_up_text"
-    t.string   "repository_storage",                    default: "default"
-    t.string   "enabled_git_access_protocol"
-    t.boolean  "domain_blacklist_enabled",              default: false
-    t.text     "domain_blacklist"
+    t.string "home_page_url"
+    t.integer "default_branch_protection", default: 2
+    t.text "restricted_visibility_levels"
+    t.boolean "version_check_enabled", default: true
+    t.integer "max_attachment_size", default: 10, null: false
+    t.integer "default_project_visibility"
+    t.integer "default_snippet_visibility"
+    t.text "domain_whitelist"
+    t.boolean "user_oauth_applications", default: true
+    t.string "after_sign_out_path"
+    t.integer "session_expire_delay", default: 10080, null: false
+    t.text "import_sources"
+    t.text "help_page_text"
+    t.string "admin_notification_email"
+    t.boolean "shared_runners_enabled", default: true, null: false
+    t.integer "max_artifacts_size", default: 100, null: false
+    t.string "runners_registration_token"
+    t.boolean "require_two_factor_authentication", default: false
+    t.integer "two_factor_grace_period", default: 48
+    t.boolean "metrics_enabled", default: false
+    t.string "metrics_host", default: "localhost"
+    t.integer "metrics_pool_size", default: 16
+    t.integer "metrics_timeout", default: 10
+    t.integer "metrics_method_call_threshold", default: 10
+    t.boolean "recaptcha_enabled", default: false
+    t.string "recaptcha_site_key"
+    t.string "recaptcha_private_key"
+    t.integer "metrics_port", default: 8089
+    t.boolean "akismet_enabled", default: false
+    t.string "akismet_api_key"
+    t.integer "metrics_sample_interval", default: 15
+    t.boolean "sentry_enabled", default: false
+    t.string "sentry_dsn"
+    t.boolean "email_author_in_body", default: false
+    t.integer "default_group_visibility"
+    t.boolean "repository_checks_enabled", default: false
+    t.text "shared_runners_text"
+    t.integer "metrics_packet_size", default: 1
+    t.text "disabled_oauth_sign_in_sources"
+    t.string "health_check_access_token"
+    t.boolean "send_user_confirmation_email", default: false
+    t.integer "container_registry_token_expire_delay", default: 5
+    t.text "after_sign_up_text"
+    t.boolean "user_default_external", default: false, null: false
+    t.string "repository_storages", default: "default"
+    t.string "enabled_git_access_protocol"
+    t.boolean "domain_blacklist_enabled", default: false
+    t.text "domain_blacklist"
+    t.boolean "koding_enabled"
+    t.string "koding_url"
+    t.text "sign_in_text_html"
+    t.text "help_page_text_html"
+    t.text "shared_runners_text_html"
+    t.text "after_sign_up_text_html"
+    t.boolean "housekeeping_enabled", default: true, null: false
+    t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
+    t.integer "housekeeping_incremental_repack_period", default: 10, null: false
+    t.integer "housekeeping_full_repack_period", default: 50, null: false
+    t.integer "housekeeping_gc_period", default: 200, null: false
   end
 
   create_table "audit_events", force: :cascade do |t|
-    t.integer  "author_id",   null: false
-    t.string   "type",        null: false
-    t.integer  "entity_id",   null: false
-    t.string   "entity_type", null: false
-    t.text     "details"
+    t.integer "author_id", null: false
+    t.string "type", null: false
+    t.integer "entity_id", null: false
+    t.string "entity_type", null: false
+    t.text "details"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -105,10 +118,10 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
 
   create_table "award_emoji", force: :cascade do |t|
-    t.string   "name"
-    t.integer  "user_id"
-    t.integer  "awardable_id"
-    t.string   "awardable_type"
+    t.string "name"
+    t.integer "user_id"
+    t.integer "awardable_id"
+    t.string "awardable_type"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -117,60 +130,71 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
   add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
 
+  create_table "boards", force: :cascade do |t|
+    t.integer "project_id", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
+
   create_table "broadcast_messages", force: :cascade do |t|
-    t.text     "message",    null: false
+    t.text "message", null: false
     t.datetime "starts_at"
     t.datetime "ends_at"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "color"
-    t.string   "font"
+    t.string "color"
+    t.string "font"
+    t.text "message_html"
   end
 
   create_table "ci_application_settings", force: :cascade do |t|
-    t.boolean  "all_broken_builds"
-    t.boolean  "add_pusher"
+    t.boolean "all_broken_builds"
+    t.boolean "add_pusher"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
 
   create_table "ci_builds", force: :cascade do |t|
-    t.integer  "project_id"
-    t.string   "status"
+    t.integer "project_id"
+    t.string "status"
     t.datetime "finished_at"
-    t.text     "trace"
+    t.text "trace"
     t.datetime "created_at"
     t.datetime "updated_at"
     t.datetime "started_at"
-    t.integer  "runner_id"
-    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"
-    t.integer  "trigger_request_id"
-    t.integer  "stage_idx"
-    t.boolean  "tag"
-    t.string   "ref"
-    t.integer  "user_id"
-    t.string   "type"
-    t.string   "target_url"
-    t.string   "description"
-    t.text     "artifacts_file"
-    t.integer  "gl_project_id"
-    t.text     "artifacts_metadata"
-    t.integer  "erased_by_id"
+    t.integer "runner_id"
+    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"
+    t.integer "trigger_request_id"
+    t.integer "stage_idx"
+    t.boolean "tag"
+    t.string "ref"
+    t.integer "user_id"
+    t.string "type"
+    t.string "target_url"
+    t.string "description"
+    t.text "artifacts_file"
+    t.integer "gl_project_id"
+    t.text "artifacts_metadata"
+    t.integer "erased_by_id"
     t.datetime "erased_at"
-    t.string   "environment"
     t.datetime "artifacts_expire_at"
-    t.integer  "artifacts_size"
-    t.string   "when"
-    t.text     "yaml_variables"
+    t.string "environment"
+    t.integer "artifacts_size", limit: 8
+    t.string "when"
+    t.text "yaml_variables"
     t.datetime "queued_at"
+    t.string "token"
+    t.integer "lock_version"
   end
 
   add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -182,24 +206,26 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   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"], 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"
-    t.string   "ref"
-    t.string   "sha"
-    t.string   "before_sha"
-    t.text     "push_data"
+    t.integer "project_id"
+    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.boolean "tag", default: false
+    t.text "yaml_errors"
     t.datetime "committed_at"
-    t.integer  "gl_project_id"
-    t.string   "status"
+    t.integer "gl_project_id"
+    t.string "status"
     t.datetime "started_at"
     t.datetime "finished_at"
-    t.integer  "duration"
-    t.integer  "user_id"
+    t.integer "duration"
+    t.integer "user_id"
+    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
@@ -209,157 +235,140 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   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.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.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.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.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"
+    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
 
   create_table "ci_runner_projects", force: :cascade do |t|
-    t.integer  "runner_id",     null: false
-    t.integer  "project_id"
+    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 "gl_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", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
 
   create_table "ci_runners", force: :cascade do |t|
-    t.string   "token"
+    t.string "token"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "description"
+    t.string "description"
     t.datetime "contacted_at"
-    t.boolean  "active",       default: true,  null: false
-    t.boolean  "is_shared",    default: false
-    t.string   "name"
-    t.string   "version"
-    t.string   "revision"
-    t.string   "platform"
-    t.string   "architecture"
-    t.boolean  "run_untagged", default: true,  null: false
-    t.boolean  "locked",       default: false, null: false
+    t.boolean "active", default: true, null: false
+    t.boolean "is_shared", default: false
+    t.string "name"
+    t.string "version"
+    t.string "revision"
+    t.string "platform"
+    t.string "architecture"
+    t.boolean "run_untagged", default: true, null: false
+    t.boolean "locked", default: false, null: false
   end
 
   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_services", force: :cascade do |t|
-    t.string   "type"
-    t.string   "title"
-    t.integer  "project_id",                 null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-    t.boolean  "active",     default: false, null: false
-    t.text     "properties"
-  end
-
   create_table "ci_sessions", force: :cascade do |t|
-    t.string   "session_id", null: false
-    t.text     "data"
+    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.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.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"
+    t.integer "trigger_id", null: false
+    t.text "variables"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "commit_id"
+    t.integer "commit_id"
   end
 
   create_table "ci_triggers", force: :cascade do |t|
-    t.string   "token"
-    t.integer  "project_id"
+    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 "gl_project_id"
   end
 
   add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_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.string "key"
+    t.text "value"
+    t.text "encrypted_value"
+    t.string "encrypted_value_salt"
+    t.string "encrypted_value_iv"
     t.integer "gl_project_id"
   end
 
   add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
 
-  create_table "ci_web_hooks", force: :cascade do |t|
-    t.string   "url",        null: false
-    t.integer  "project_id", null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
   create_table "deploy_keys_projects", force: :cascade do |t|
-    t.integer  "deploy_key_id", null: false
-    t.integer  "project_id",    null: false
+    t.integer "deploy_key_id", null: false
+    t.integer "project_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -367,17 +376,18 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
 
   create_table "deployments", force: :cascade do |t|
-    t.integer  "iid",             null: false
-    t.integer  "project_id",      null: false
-    t.integer  "environment_id",  null: false
-    t.string   "ref",             null: false
-    t.boolean  "tag",             null: false
-    t.string   "sha",             null: false
-    t.integer  "user_id"
-    t.integer  "deployable_id"
-    t.string   "deployable_type"
+    t.integer "iid", null: false
+    t.integer "project_id", null: false
+    t.integer "environment_id", null: false
+    t.string "ref", null: false
+    t.boolean "tag", null: false
+    t.string "sha", null: false
+    t.integer "user_id"
+    t.integer "deployable_id"
+    t.string "deployable_type"
     t.datetime "created_at"
     t.datetime "updated_at"
+    t.string "on_stop"
   end
 
   add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
@@ -386,8 +396,8 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
 
   create_table "emails", force: :cascade do |t|
-    t.integer  "user_id",    null: false
-    t.string   "email",      null: false
+    t.integer "user_id", null: false
+    t.string "email", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -396,25 +406,27 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
 
   create_table "environments", force: :cascade do |t|
-    t.integer  "project_id"
-    t.string   "name",         null: false
+    t.integer "project_id"
+    t.string "name", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "external_url"
+    t.string "external_url"
+    t.string "environment_type"
+    t.string "state", default: "available", null: false
   end
 
   add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
 
   create_table "events", force: :cascade do |t|
-    t.string   "target_type"
-    t.integer  "target_id"
-    t.string   "title"
-    t.text     "data"
-    t.integer  "project_id"
+    t.string "target_type"
+    t.integer "target_id"
+    t.string "title"
+    t.text "data"
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "action"
-    t.integer  "author_id"
+    t.integer "action"
+    t.integer "author_id"
   end
 
   add_index "events", ["action"], name: "index_events_on_action", using: :btree
@@ -425,8 +437,8 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
 
   create_table "forked_project_links", force: :cascade do |t|
-    t.integer  "forked_to_project_id",   null: false
-    t.integer  "forked_from_project_id", null: false
+    t.integer "forked_to_project_id", null: false
+    t.integer "forked_from_project_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -434,33 +446,47 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
 
   create_table "identities", force: :cascade do |t|
-    t.string   "extern_uid"
-    t.string   "provider"
-    t.integer  "user_id"
+    t.string "extern_uid"
+    t.string "provider"
+    t.integer "user_id"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
 
   add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
 
+  create_table "issue_metrics", force: :cascade do |t|
+    t.integer "issue_id", null: false
+    t.datetime "first_mentioned_in_commit_at"
+    t.datetime "first_associated_with_milestone_at"
+    t.datetime "first_added_to_board_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
+
   create_table "issues", force: :cascade do |t|
-    t.string   "title"
-    t.integer  "assignee_id"
-    t.integer  "author_id"
-    t.integer  "project_id"
+    t.string "title"
+    t.integer "assignee_id"
+    t.integer "author_id"
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "position",      default: 0
-    t.string   "branch_name"
-    t.text     "description"
-    t.integer  "milestone_id"
-    t.string   "state"
-    t.integer  "iid"
-    t.integer  "updated_by_id"
-    t.boolean  "confidential",  default: false
+    t.integer "position", default: 0
+    t.string "branch_name"
+    t.text "description"
+    t.integer "milestone_id"
+    t.string "state"
+    t.integer "iid"
+    t.integer "updated_by_id"
+    t.boolean "confidential", default: false
     t.datetime "deleted_at"
-    t.date     "due_date"
-    t.integer  "moved_to_id"
+    t.date "due_date"
+    t.integer "moved_to_id"
+    t.integer "lock_version"
+    t.text "title_html"
+    t.text "description_html"
   end
 
   add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -476,24 +502,24 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
   create_table "keys", force: :cascade do |t|
-    t.integer  "user_id"
+    t.integer "user_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.text     "key"
-    t.string   "title"
-    t.string   "type"
-    t.string   "fingerprint"
-    t.boolean  "public",      default: false, null: false
-    t.boolean  "can_push",    default: false, null: false
+    t.text "key"
+    t.string "title"
+    t.string "type"
+    t.string "fingerprint"
+    t.boolean "public", default: false, null: false
+    t.boolean "can_push", default: false, null: false
   end
 
   add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
   add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
 
   create_table "label_links", force: :cascade do |t|
-    t.integer  "label_id"
-    t.integer  "target_id"
-    t.string   "target_type"
+    t.integer "label_id"
+    t.integer "target_id"
+    t.string "target_type"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -501,53 +527,80 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
   add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
 
+  create_table "label_priorities", force: :cascade do |t|
+    t.integer "project_id", null: false
+    t.integer "label_id", null: false
+    t.integer "priority", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
+  add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
+
   create_table "labels", force: :cascade do |t|
-    t.string   "title"
-    t.string   "color"
-    t.integer  "project_id"
+    t.string "title"
+    t.string "color"
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.boolean  "template",    default: false
-    t.string   "description"
-    t.integer  "priority"
+    t.boolean "template", default: false
+    t.string "description"
+    t.text "description_html"
+    t.string "type"
+    t.integer "group_id"
   end
 
-  add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
-  add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+  add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+  add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree
 
   create_table "lfs_objects", force: :cascade do |t|
-    t.string   "oid",                  null: false
-    t.integer  "size",       limit: 8, null: false
+    t.string "oid", null: false
+    t.integer "size", limit: 8, null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "file"
+    t.string "file"
   end
 
   add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
 
   create_table "lfs_objects_projects", force: :cascade do |t|
-    t.integer  "lfs_object_id", null: false
-    t.integer  "project_id",    null: false
+    t.integer "lfs_object_id", null: false
+    t.integer "project_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
 
   add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
 
+  create_table "lists", force: :cascade do |t|
+    t.integer "board_id", null: false
+    t.integer "label_id"
+    t.integer "list_type", default: 1, null: false
+    t.integer "position"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
+  add_index "lists", ["board_id"], name: "index_lists_on_board_id", using: :btree
+  add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
+
   create_table "members", force: :cascade do |t|
-    t.integer  "access_level",       null: false
-    t.integer  "source_id",          null: false
-    t.string   "source_type",        null: false
-    t.integer  "user_id"
-    t.integer  "notification_level", null: false
-    t.string   "type"
+    t.integer "access_level", null: false
+    t.integer "source_id", null: false
+    t.string "source_type", null: false
+    t.integer "user_id"
+    t.integer "notification_level", null: false
+    t.string "type"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "created_by_id"
-    t.string   "invite_email"
-    t.string   "invite_token"
+    t.integer "created_by_id"
+    t.string "invite_email"
+    t.string "invite_token"
     t.datetime "invite_accepted_at"
     t.datetime "requested_at"
+    t.date "expires_at"
   end
 
   add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -557,45 +610,61 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
 
   create_table "merge_request_diffs", force: :cascade do |t|
-    t.string   "state"
-    t.text     "st_commits"
-    t.text     "st_diffs"
-    t.integer  "merge_request_id", null: false
+    t.string "state"
+    t.text "st_commits"
+    t.text "st_diffs"
+    t.integer "merge_request_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "base_commit_sha"
-    t.string   "real_size"
-    t.string   "head_commit_sha"
-    t.string   "start_commit_sha"
+    t.string "base_commit_sha"
+    t.string "real_size"
+    t.string "head_commit_sha"
+    t.string "start_commit_sha"
+  end
+
+  add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+
+  create_table "merge_request_metrics", force: :cascade do |t|
+    t.integer "merge_request_id", null: false
+    t.datetime "latest_build_started_at"
+    t.datetime "latest_build_finished_at"
+    t.datetime "first_deployed_to_production_at"
+    t.datetime "merged_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
-  add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+  add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
+  add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
 
   create_table "merge_requests", force: :cascade do |t|
-    t.string   "target_branch",                                null: false
-    t.string   "source_branch",                                null: false
-    t.integer  "source_project_id",                            null: false
-    t.integer  "author_id"
-    t.integer  "assignee_id"
-    t.string   "title"
+    t.string "target_branch", null: false
+    t.string "source_branch", null: false
+    t.integer "source_project_id", null: false
+    t.integer "author_id"
+    t.integer "assignee_id"
+    t.string "title"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "milestone_id"
-    t.string   "state"
-    t.string   "merge_status"
-    t.integer  "target_project_id",                            null: false
-    t.integer  "iid"
-    t.text     "description"
-    t.integer  "position",                     default: 0
+    t.integer "milestone_id"
+    t.string "state"
+    t.string "merge_status"
+    t.integer "target_project_id", null: false
+    t.integer "iid"
+    t.text "description"
+    t.integer "position", default: 0
     t.datetime "locked_at"
-    t.integer  "updated_by_id"
-    t.string   "merge_error"
-    t.text     "merge_params"
-    t.boolean  "merge_when_build_succeeds",    default: false, null: false
-    t.integer  "merge_user_id"
-    t.string   "merge_commit_sha"
+    t.integer "updated_by_id"
+    t.text "merge_error"
+    t.text "merge_params"
+    t.boolean "merge_when_build_succeeds", default: false, null: false
+    t.integer "merge_user_id"
+    t.string "merge_commit_sha"
     t.datetime "deleted_at"
-    t.string   "in_progress_merge_commit_sha"
+    t.string "in_progress_merge_commit_sha"
+    t.integer "lock_version"
+    t.text "title_html"
+    t.text "description_html"
   end
 
   add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -611,15 +680,27 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
   add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
+  create_table "merge_requests_closing_issues", force: :cascade do |t|
+    t.integer "merge_request_id", null: false
+    t.integer "issue_id", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
+  add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
+
   create_table "milestones", force: :cascade do |t|
-    t.string   "title",       null: false
-    t.integer  "project_id",  null: false
-    t.text     "description"
-    t.date     "due_date"
+    t.string "title", null: false
+    t.integer "project_id", null: false
+    t.text "description"
+    t.date "due_date"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "state"
-    t.integer  "iid"
+    t.string "state"
+    t.integer "iid"
+    t.text "title_html"
+    t.text "description_html"
   end
 
   add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -630,18 +711,20 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
   create_table "namespaces", force: :cascade do |t|
-    t.string   "name",                                   null: false
-    t.string   "path",                                   null: false
-    t.integer  "owner_id"
+    t.string "name", null: false
+    t.string "path", null: false
+    t.integer "owner_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "type"
-    t.string   "description",            default: "",    null: false
-    t.string   "avatar"
-    t.boolean  "share_with_group_lock",  default: false
-    t.integer  "visibility_level",       default: 20,    null: false
-    t.boolean  "request_access_enabled", default: true,  null: false
+    t.string "type"
+    t.string "description", default: "", null: false
+    t.string "avatar"
+    t.boolean "share_with_group_lock", default: false
+    t.integer "visibility_level", default: 20, null: false
+    t.boolean "request_access_enabled", default: true, null: false
     t.datetime "deleted_at"
+    t.boolean "lfs_enabled"
+    t.text "description_html"
   end
 
   add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -654,27 +737,33 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
 
   create_table "notes", force: :cascade do |t|
-    t.text     "note"
-    t.string   "noteable_type"
-    t.integer  "author_id"
+    t.text "note"
+    t.string "noteable_type"
+    t.integer "author_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "project_id"
-    t.string   "attachment"
-    t.string   "line_code"
-    t.string   "commit_id"
-    t.integer  "noteable_id"
-    t.boolean  "system",            default: false, null: false
-    t.text     "st_diff"
-    t.integer  "updated_by_id"
-    t.string   "type"
-    t.text     "position"
-    t.text     "original_position"
+    t.integer "project_id"
+    t.string "attachment"
+    t.string "line_code"
+    t.string "commit_id"
+    t.integer "noteable_id"
+    t.boolean "system", default: false, null: false
+    t.text "st_diff"
+    t.integer "updated_by_id"
+    t.string "type"
+    t.text "position"
+    t.text "original_position"
+    t.datetime "resolved_at"
+    t.integer "resolved_by_id"
+    t.string "discussion_id"
+    t.string "original_discussion_id"
+    t.text "note_html"
   end
 
   add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
   add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
   add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+  add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
   add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
   add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
   add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
@@ -684,13 +773,13 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
 
   create_table "notification_settings", force: :cascade do |t|
-    t.integer  "user_id",                 null: false
-    t.integer  "source_id"
-    t.string   "source_type"
-    t.integer  "level",       default: 0, null: false
-    t.datetime "created_at",              null: false
-    t.datetime "updated_at",              null: false
-    t.text     "events"
+    t.integer "user_id", null: false
+    t.integer "source_id"
+    t.string "source_type"
+    t.integer "level", default: 0, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.text "events"
   end
 
   add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -698,27 +787,27 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
 
   create_table "oauth_access_grants", force: :cascade do |t|
-    t.integer  "resource_owner_id", null: false
-    t.integer  "application_id",    null: false
-    t.string   "token",             null: false
-    t.integer  "expires_in",        null: false
-    t.text     "redirect_uri",      null: false
-    t.datetime "created_at",        null: false
+    t.integer "resource_owner_id", null: false
+    t.integer "application_id", null: false
+    t.string "token", null: false
+    t.integer "expires_in", null: false
+    t.text "redirect_uri", null: false
+    t.datetime "created_at", null: false
     t.datetime "revoked_at"
-    t.string   "scopes"
+    t.string "scopes"
   end
 
   add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
 
   create_table "oauth_access_tokens", force: :cascade do |t|
-    t.integer  "resource_owner_id"
-    t.integer  "application_id"
-    t.string   "token",             null: false
-    t.string   "refresh_token"
-    t.integer  "expires_in"
+    t.integer "resource_owner_id"
+    t.integer "application_id"
+    t.string "token", null: false
+    t.string "refresh_token"
+    t.integer "expires_in"
     t.datetime "revoked_at"
-    t.datetime "created_at",        null: false
-    t.string   "scopes"
+    t.datetime "created_at", null: false
+    t.string "scopes"
   end
 
   add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
@@ -726,91 +815,105 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
 
   create_table "oauth_applications", force: :cascade do |t|
-    t.string   "name",                      null: false
-    t.string   "uid",                       null: false
-    t.string   "secret",                    null: false
-    t.text     "redirect_uri",              null: false
-    t.string   "scopes",       default: "", null: false
+    t.string "name", null: false
+    t.string "uid", null: false
+    t.string "secret", null: false
+    t.text "redirect_uri", null: false
+    t.string "scopes", default: "", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "owner_id"
-    t.string   "owner_type"
+    t.integer "owner_id"
+    t.string "owner_type"
   end
 
   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 "personal_access_tokens", force: :cascade do |t|
-    t.integer  "user_id",                    null: false
-    t.string   "token",                      null: false
-    t.string   "name",                       null: false
-    t.datetime "created_at",                 null: false
-    t.datetime "updated_at",                 null: false
-    t.boolean  "revoked",    default: false
+    t.integer "user_id", null: false
+    t.string "token", null: false
+    t.string "name", null: false
+    t.boolean "revoked", default: false
     t.datetime "expires_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
   add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
   add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
 
+  create_table "project_features", force: :cascade do |t|
+    t.integer "project_id"
+    t.integer "merge_requests_access_level"
+    t.integer "issues_access_level"
+    t.integer "wiki_access_level"
+    t.integer "snippets_access_level"
+    t.integer "builds_access_level"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.integer "repository_access_level", default: 20, null: false
+  end
+
+  add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
+
   create_table "project_group_links", force: :cascade do |t|
-    t.integer  "project_id",                null: false
-    t.integer  "group_id",                  null: false
+    t.integer "project_id", null: false
+    t.integer "group_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "group_access", default: 30, null: false
+    t.integer "group_access", default: 30, null: false
+    t.date "expires_at"
   end
 
   create_table "project_import_data", force: :cascade do |t|
     t.integer "project_id"
-    t.text    "data"
-    t.text    "encrypted_credentials"
-    t.string  "encrypted_credentials_iv"
-    t.string  "encrypted_credentials_salt"
+    t.text "data"
+    t.text "encrypted_credentials"
+    t.string "encrypted_credentials_iv"
+    t.string "encrypted_credentials_salt"
   end
 
+  add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
+
   create_table "projects", force: :cascade do |t|
-    t.string   "name"
-    t.string   "path"
-    t.text     "description"
+    t.string "name"
+    t.string "path"
+    t.text "description"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "creator_id"
-    t.boolean  "issues_enabled",                     default: true,      null: false
-    t.boolean  "merge_requests_enabled",             default: true,      null: false
-    t.boolean  "wiki_enabled",                       default: true,      null: false
-    t.integer  "namespace_id"
-    t.boolean  "snippets_enabled",                   default: true,      null: false
+    t.integer "creator_id"
+    t.integer "namespace_id"
     t.datetime "last_activity_at"
-    t.string   "import_url"
-    t.integer  "visibility_level",                   default: 0,         null: false
-    t.boolean  "archived",                           default: false,     null: false
-    t.string   "avatar"
-    t.string   "import_status"
-    t.float    "repository_size",                    default: 0.0
-    t.integer  "star_count",                         default: 0,         null: false
-    t.string   "import_type"
-    t.string   "import_source"
-    t.integer  "commit_count",                       default: 0
-    t.text     "import_error"
-    t.integer  "ci_id"
-    t.boolean  "builds_enabled",                     default: true,      null: false
-    t.boolean  "shared_runners_enabled",             default: true,      null: false
-    t.string   "runners_token"
-    t.string   "build_coverage_regex"
-    t.boolean  "build_allow_git_fetch",              default: true,      null: false
-    t.integer  "build_timeout",                      default: 3600,      null: false
-    t.boolean  "pending_delete",                     default: false
-    t.boolean  "public_builds",                      default: true,      null: false
-    t.integer  "pushes_since_gc",                    default: 0
-    t.boolean  "last_repository_check_failed"
+    t.string "import_url"
+    t.integer "visibility_level", default: 0, null: false
+    t.boolean "archived", default: false, null: false
+    t.string "avatar"
+    t.string "import_status"
+    t.float "repository_size", default: 0.0
+    t.integer "star_count", default: 0, null: false
+    t.string "import_type"
+    t.string "import_source"
+    t.integer "commit_count", default: 0
+    t.text "import_error"
+    t.integer "ci_id"
+    t.boolean "shared_runners_enabled", default: true, null: false
+    t.string "runners_token"
+    t.string "build_coverage_regex"
+    t.boolean "build_allow_git_fetch", default: true, null: false
+    t.integer "build_timeout", default: 3600, null: false
+    t.boolean "pending_delete", default: false
+    t.boolean "public_builds", default: true, null: false
+    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  "has_external_issue_tracker"
-    t.string   "repository_storage",                 default: "default", null: false
-    t.boolean  "has_external_wiki"
-    t.boolean  "request_access_enabled",             default: true,      null: false
+    t.boolean "container_registry_enabled"
+    t.boolean "only_allow_merge_if_build_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: true, null: false
+    t.boolean "has_external_wiki"
+    t.boolean "lfs_enabled"
+    t.text "description_html"
+    t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false
   end
 
   add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -829,26 +932,26 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
 
   create_table "protected_branch_merge_access_levels", force: :cascade do |t|
-    t.integer  "protected_branch_id",              null: false
-    t.integer  "access_level",        default: 40, null: false
-    t.datetime "created_at",                       null: false
-    t.datetime "updated_at",                       null: false
+    t.integer "protected_branch_id", null: false
+    t.integer "access_level", default: 40, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
   add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
 
   create_table "protected_branch_push_access_levels", force: :cascade do |t|
-    t.integer  "protected_branch_id",              null: false
-    t.integer  "access_level",        default: 40, null: false
-    t.datetime "created_at",                       null: false
-    t.datetime "updated_at",                       null: false
+    t.integer "protected_branch_id", null: false
+    t.integer "access_level", default: 40, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
   add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
 
   create_table "protected_branches", force: :cascade do |t|
-    t.integer  "project_id", null: false
-    t.string   "name",       null: false
+    t.integer "project_id", null: false
+    t.string "name", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -856,11 +959,12 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
 
   create_table "releases", force: :cascade do |t|
-    t.string   "tag"
-    t.text     "description"
-    t.integer  "project_id"
+    t.string "tag"
+    t.text "description"
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
+    t.text "description_html"
   end
 
   add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
@@ -869,51 +973,54 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   create_table "sent_notifications", force: :cascade do |t|
     t.integer "project_id"
     t.integer "noteable_id"
-    t.string  "noteable_type"
+    t.string "noteable_type"
     t.integer "recipient_id"
-    t.string  "commit_id"
-    t.string  "reply_key",     null: false
-    t.string  "line_code"
-    t.string  "note_type"
-    t.text    "position"
+    t.string "commit_id"
+    t.string "reply_key", null: false
+    t.string "line_code"
+    t.string "note_type"
+    t.text "position"
   end
 
   add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
 
   create_table "services", force: :cascade do |t|
-    t.string   "type"
-    t.string   "title"
-    t.integer  "project_id"
+    t.string "type"
+    t.string "title"
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.boolean  "active",                default: false,    null: false
-    t.text     "properties"
-    t.boolean  "template",              default: false
-    t.boolean  "push_events",           default: true
-    t.boolean  "issues_events",         default: true
-    t.boolean  "merge_requests_events", default: true
-    t.boolean  "tag_push_events",       default: true
-    t.boolean  "note_events",           default: true,     null: false
-    t.boolean  "build_events",          default: false,    null: false
-    t.string   "category",              default: "common", null: false
-    t.boolean  "default",               default: false
-    t.boolean  "wiki_page_events",      default: true
-    t.boolean  "pipeline_events",       default: false,    null: false
+    t.boolean "active", default: false, null: false
+    t.text "properties"
+    t.boolean "template", default: false
+    t.boolean "push_events", default: true
+    t.boolean "issues_events", default: true
+    t.boolean "merge_requests_events", default: true
+    t.boolean "tag_push_events", default: true
+    t.boolean "note_events", default: true, null: false
+    t.boolean "build_events", default: false, null: false
+    t.string "category", default: "common", null: false
+    t.boolean "default", default: false
+    t.boolean "wiki_page_events", default: true
+    t.boolean "pipeline_events", default: false, null: false
+    t.boolean "confidential_issues_events", default: true, null: false
   end
 
   add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
   add_index "services", ["template"], name: "index_services_on_template", using: :btree
 
   create_table "snippets", force: :cascade do |t|
-    t.string   "title"
-    t.text     "content"
-    t.integer  "author_id",                    null: false
-    t.integer  "project_id"
+    t.string "title"
+    t.text "content"
+    t.integer "author_id", null: false
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "file_name"
-    t.string   "type"
-    t.integer  "visibility_level", default: 0, null: false
+    t.string "file_name"
+    t.string "type"
+    t.integer "visibility_level", default: 0, null: false
+    t.text "title_html"
+    t.text "content_html"
   end
 
   add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -924,23 +1031,23 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
 
   create_table "spam_logs", force: :cascade do |t|
-    t.integer  "user_id"
-    t.string   "source_ip"
-    t.string   "user_agent"
-    t.boolean  "via_api"
-    t.string   "noteable_type"
-    t.string   "title"
-    t.text     "description"
-    t.datetime "created_at",                       null: false
-    t.datetime "updated_at",                       null: false
-    t.boolean  "submitted_as_ham", default: false, null: false
+    t.integer "user_id"
+    t.string "source_ip"
+    t.string "user_agent"
+    t.boolean "via_api"
+    t.string "noteable_type"
+    t.string "title"
+    t.text "description"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.boolean "submitted_as_ham", default: false, null: false
   end
 
   create_table "subscriptions", force: :cascade do |t|
-    t.integer  "user_id"
-    t.integer  "subscribable_id"
-    t.string   "subscribable_type"
-    t.boolean  "subscribed"
+    t.integer "user_id"
+    t.integer "subscribable_id"
+    t.string "subscribable_type"
+    t.boolean "subscribed"
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -948,12 +1055,12 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
 
   create_table "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"
+    t.integer "tag_id"
+    t.integer "taggable_id"
+    t.string "taggable_type"
+    t.integer "tagger_id"
+    t.string "tagger_type"
+    t.string "context"
     t.datetime "created_at"
   end
 
@@ -961,24 +1068,24 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
 
   create_table "tags", force: :cascade do |t|
-    t.string  "name"
+    t.string "name"
     t.integer "taggings_count", default: 0
   end
 
   add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
 
   create_table "todos", force: :cascade do |t|
-    t.integer  "user_id",     null: false
-    t.integer  "project_id",  null: false
-    t.integer  "target_id"
-    t.string   "target_type", null: false
-    t.integer  "author_id"
-    t.integer  "action",      null: false
-    t.string   "state",       null: false
+    t.integer "user_id", null: false
+    t.integer "project_id", null: false
+    t.integer "target_id"
+    t.string "target_type", null: false
+    t.integer "author_id"
+    t.integer "action", null: false
+    t.string "state", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer  "note_id"
-    t.string   "commit_id"
+    t.integer "note_id"
+    t.string "commit_id"
   end
 
   add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
@@ -988,87 +1095,96 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
   add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
 
+  create_table "trending_projects", force: :cascade do |t|
+    t.integer "project_id", null: false
+  end
+
+  add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
+
   create_table "u2f_registrations", force: :cascade do |t|
-    t.text     "certificate"
-    t.string   "key_handle"
-    t.string   "public_key"
-    t.integer  "counter"
-    t.integer  "user_id"
-    t.datetime "created_at",  null: false
-    t.datetime "updated_at",  null: false
+    t.text "certificate"
+    t.string "key_handle"
+    t.string "public_key"
+    t.integer "counter"
+    t.integer "user_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "name"
   end
 
   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 "user_agent_details", force: :cascade do |t|
-    t.string   "user_agent",                   null: false
-    t.string   "ip_address",                   null: false
-    t.integer  "subject_id",                   null: false
-    t.string   "subject_type",                 null: false
-    t.boolean  "submitted",    default: false, null: false
-    t.datetime "created_at",                   null: false
-    t.datetime "updated_at",                   null: false
+    t.string "user_agent", null: false
+    t.string "ip_address", null: false
+    t.integer "subject_id", null: false
+    t.string "subject_type", null: false
+    t.boolean "submitted", default: false, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
   end
 
   create_table "users", force: :cascade do |t|
-    t.string   "email",                       default: "",    null: false
-    t.string   "encrypted_password",          default: "",    null: false
-    t.string   "reset_password_token"
+    t.string "email", default: "", null: false
+    t.string "encrypted_password", default: "", null: false
+    t.string "reset_password_token"
     t.datetime "reset_password_sent_at"
     t.datetime "remember_created_at"
-    t.integer  "sign_in_count",               default: 0
+    t.integer "sign_in_count", default: 0
     t.datetime "current_sign_in_at"
     t.datetime "last_sign_in_at"
-    t.string   "current_sign_in_ip"
-    t.string   "last_sign_in_ip"
+    t.string "current_sign_in_ip"
+    t.string "last_sign_in_ip"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "name"
-    t.boolean  "admin",                       default: false, null: false
-    t.integer  "projects_limit",              default: 10
-    t.string   "skype",                       default: "",    null: false
-    t.string   "linkedin",                    default: "",    null: false
-    t.string   "twitter",                     default: "",    null: false
-    t.string   "authentication_token"
-    t.integer  "theme_id",                    default: 1,     null: false
-    t.string   "bio"
-    t.integer  "failed_attempts",             default: 0
+    t.string "name"
+    t.boolean "admin", default: false, null: false
+    t.integer "projects_limit", default: 10
+    t.string "skype", default: "", null: false
+    t.string "linkedin", default: "", null: false
+    t.string "twitter", default: "", null: false
+    t.string "authentication_token"
+    t.integer "theme_id", default: 1, null: false
+    t.string "bio"
+    t.integer "failed_attempts", default: 0
     t.datetime "locked_at"
-    t.string   "username"
-    t.boolean  "can_create_group",            default: true,  null: false
-    t.boolean  "can_create_team",             default: true,  null: false
-    t.string   "state"
-    t.integer  "color_scheme_id",             default: 1,     null: false
+    t.string "username"
+    t.boolean "can_create_group", default: true, null: false
+    t.boolean "can_create_team", default: true, null: false
+    t.string "state"
+    t.integer "color_scheme_id", default: 1, null: false
     t.datetime "password_expires_at"
-    t.integer  "created_by_id"
+    t.integer "created_by_id"
     t.datetime "last_credential_check_at"
-    t.string   "avatar"
-    t.string   "confirmation_token"
+    t.string "avatar"
+    t.string "confirmation_token"
     t.datetime "confirmed_at"
     t.datetime "confirmation_sent_at"
-    t.string   "unconfirmed_email"
-    t.boolean  "hide_no_ssh_key",             default: false
-    t.string   "website_url",                 default: "",    null: false
-    t.string   "notification_email"
-    t.boolean  "hide_no_password",            default: false
-    t.boolean  "password_automatically_set",  default: false
-    t.string   "location"
-    t.string   "encrypted_otp_secret"
-    t.string   "encrypted_otp_secret_iv"
-    t.string   "encrypted_otp_secret_salt"
-    t.boolean  "otp_required_for_login",      default: false, null: false
-    t.text     "otp_backup_codes"
-    t.string   "public_email",                default: "",    null: false
-    t.integer  "dashboard",                   default: 0
-    t.integer  "project_view",                default: 0
-    t.integer  "consumed_timestep"
-    t.integer  "layout",                      default: 0
-    t.boolean  "hide_project_limit",          default: false
-    t.string   "unlock_token"
+    t.string "unconfirmed_email"
+    t.boolean "hide_no_ssh_key", default: false
+    t.string "website_url", default: "", null: false
+    t.string "notification_email"
+    t.boolean "hide_no_password", default: false
+    t.boolean "password_automatically_set", default: false
+    t.string "location"
+    t.string "encrypted_otp_secret"
+    t.string "encrypted_otp_secret_iv"
+    t.string "encrypted_otp_secret_salt"
+    t.boolean "otp_required_for_login", default: false, null: false
+    t.text "otp_backup_codes"
+    t.string "public_email", default: "", null: false
+    t.integer "dashboard", default: 0
+    t.integer "project_view", default: 0
+    t.integer "consumed_timestep"
+    t.integer "layout", default: 0
+    t.boolean "hide_project_limit", default: false
+    t.string "unlock_token"
     t.datetime "otp_grace_period_started_at"
-    t.boolean  "ldap_email",                  default: false, null: false
-    t.boolean  "external",                    default: false
+    t.boolean "ldap_email", default: false, null: false
+    t.boolean "external", default: false
+    t.string "organization"
+    t.string "incoming_email_token"
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1078,6 +1194,7 @@ ActiveRecord::Schema.define(version: 20160811172945) 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", ["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"}
   add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
@@ -1086,8 +1203,8 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
 
   create_table "users_star_projects", force: :cascade do |t|
-    t.integer  "project_id", null: false
-    t.integer  "user_id",    null: false
+    t.integer "project_id", null: false
+    t.integer "user_id", null: false
     t.datetime "created_at"
     t.datetime "updated_at"
   end
@@ -1097,28 +1214,40 @@ ActiveRecord::Schema.define(version: 20160811172945) do
   add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
 
   create_table "web_hooks", force: :cascade do |t|
-    t.string   "url",                     limit: 2000
-    t.integer  "project_id"
+    t.string "url", limit: 2000
+    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.string   "type",                                 default: "ProjectHook"
-    t.integer  "service_id"
-    t.boolean  "push_events",                          default: true,          null: false
-    t.boolean  "issues_events",                        default: false,         null: false
-    t.boolean  "merge_requests_events",                default: false,         null: false
-    t.boolean  "tag_push_events",                      default: false
-    t.boolean  "note_events",                          default: false,         null: false
-    t.boolean  "enable_ssl_verification",              default: true
-    t.boolean  "build_events",                         default: false,         null: false
-    t.boolean  "wiki_page_events",                     default: false,         null: false
-    t.string   "token"
-    t.boolean  "pipeline_events",                      default: false,         null: false
+    t.string "type", default: "ProjectHook"
+    t.integer "service_id"
+    t.boolean "push_events", default: true, null: false
+    t.boolean "issues_events", default: false, null: false
+    t.boolean "merge_requests_events", default: false, null: false
+    t.boolean "tag_push_events", default: false
+    t.boolean "note_events", default: false, null: false
+    t.boolean "enable_ssl_verification", default: true
+    t.boolean "build_events", default: false, null: false
+    t.boolean "wiki_page_events", default: false, null: false
+    t.string "token"
+    t.boolean "pipeline_events", default: false, null: false
+    t.boolean "confidential_issues_events", default: false, null: false
   end
 
   add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
 
+  add_foreign_key "boards", "projects"
+  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", "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 "personal_access_tokens", "users"
   add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
   add_foreign_key "protected_branch_push_access_levels", "protected_branches"
+  add_foreign_key "trending_projects", "projects", on_delete: :cascade
   add_foreign_key "u2f_registrations", "users"
 end
diff --git a/doc/README.md b/doc/README.md
index fc51ea911b987c914e052fdc1d0a13c388e987e4..66c8c26e4f0611291781f7bc55e386d58cdabdde 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -2,11 +2,12 @@
 
 ## User documentation
 
+- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
 - [API](api/README.md) Automate GitLab via a simple and powerful API.
 - [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
 - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
-- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
+- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
 - [Importing to GitLab](workflow/importing/README.md).
 - [Importing and exporting projects between instances](user/project/settings/import_export.md).
 - [Markdown](user/markdown.md) GitLab's advanced formatting system.
@@ -18,6 +19,9 @@
 - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
 - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
 - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+- [University](university/README.md) Learn Git and GitLab through videos and courses.
+- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
+- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
 
 ## Administrator documentation
 
@@ -28,11 +32,12 @@
 - [Install](install/README.md) Requirements, directory structures and installation from source.
 - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
 - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
-- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
+- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
+- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
+- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
 - [Log system](administration/logs.md) Log system.
 - [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Operations](operations/README.md) Keeping GitLab up and running.
+- [Operations](administration/operations.md) Keeping GitLab up and running.
 - [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
 - [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
 - [Repository storages](administration/repository_storages.md) Manage the paths used to store repositories.
@@ -40,12 +45,13 @@
 - [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.
-- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [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)
 - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
+- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
 - [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
 - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
 - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 7186f707ad66c75cbc1e45054640473e8a043f7e..fd23047f0271da7501b95784d49b807994c598ed 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -35,6 +35,10 @@ of one hour.
 To enable LDAP integration you need to add your LDAP server settings in
 `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
 
+There is a Rake task to check LDAP configuration. After configuring LDAP
+using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check)
+for information on the LDAP check Rake task.
+
 >**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
 one GitLab server.
 
@@ -275,3 +279,9 @@ If you are getting 'Connection Refused' errors when trying to connect to the
 LDAP server please double-check the LDAP `port` and `method` settings used by
 GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
 `method: 'ssl'` and `port: 636`.
+
+### Login with valid credentials rejected
+
+If there is an unexpected error while authenticating the user with the LDAP
+backend, the login is rejected and details about the error are logged to
+`production.log`.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 28c4c7c86ca4c1fe543c7b973b0205769155353f..d7cfb464f743bb3d7baf05ba03955a99588e93b0 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,42 +1,32 @@
-# GitLab Container Registry Administration
+# GitLab Container Registry administration
 
 > [Introduced][ce-4040] in GitLab 8.8.
 
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
 ---
 
-<!-- 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)*
+> **Notes:**
+- Container Registry manifest `v1` support was added in GitLab 8.9 to support
+  Docker versions earlier than 1.10.
+- This document is about the admin guide. To learn how to use GitLab Container
+  Registry [user documentation](../user/project/container_registry.md).
 
-- [Enable the Container Registry](#enable-the-container-registry)
-- [Container Registry domain configuration](#container-registry-domain-configuration)
-    - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain)
-    - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain)
-- [Disable Container Registry site-wide](#disable-container-registry-site-wide)
-- [Disable Container Registry per project](#disable-container-registry-per-project)
-- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
-- [Container Registry storage path](#container-registry-storage-path)
-- [Container Registry storage driver](#container-registry-storage-driver)
-- [Storage limitations](#storage-limitations)
-- [Changelog](#changelog)
+With the Container Registry integrated into GitLab, every project can have its
+own space to store its Docker images.
 
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+You can read more about the Container Registry at
+https://docs.docker.com/registry/introduction/.
 
 ## Enable the Container Registry
 
 **Omnibus GitLab installations**
 
 All you have to do is configure the domain name under which the Container
-Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration)
+Registry will listen to. Read
+[#container-registry-domain-configuration](#container-registry-domain-configuration)
 and pick one of the two options that fits your case.
 
 >**Note:**
-The container Registry works under HTTPS by default. Using HTTP is possible
+The container registry works under HTTPS by default. Using HTTP is possible
 but not recommended and out of the scope of this document.
 Read the [insecure Registry documentation][docker-insecure] if you want to
 implement this.
@@ -47,7 +37,7 @@ implement this.
 
 If you have installed GitLab from source:
 
-1. You will have to [install Docker Registry][registry-deploy] by yourself.
+1. You will have to [install Registry][registry-deploy] by yourself.
 1. After the installation is complete, you will have to configure the Registry's
    settings in `gitlab.yml` in order to enable it.
 1. Use the sample NGINX configuration file that is found under
@@ -80,11 +70,13 @@ where:
 | `issuer`  | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
 
 >**Note:**
-GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab]
-will not restart the Registry should you modify its settings. Read the upstream
-documentation on how to achieve that.
+A Registry init file is not shipped with GitLab if you install it from source.
+Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
+you modify its settings. Read the upstream documentation on how to achieve that.
 
-The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+At the absolute minimum, make sure your [Registry configuration][registry-auth]
+has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
+as the realm:
 
 ```
 auth:
@@ -275,12 +267,6 @@ Registry application itself.
 
 1. Save the file and [restart GitLab][] for the changes to take effect.
 
-## Disable Container Registry per project
-
-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 for new projects site-wide
 
 If the Container Registry is enabled, then it will be available on all new
@@ -406,7 +392,8 @@ To configure the storage driver in Omnibus:
       's3' => {
         'accesskey' => 's3-access-key',
         'secretkey' => 's3-secret-key-for-access-key',
-        'bucket' => 'your-s3-bucket'
+        'bucket' => 'your-s3-bucket',
+        'region' => 'your-s3-region'
       }
     }
     ```
@@ -428,12 +415,53 @@ storage:
     accesskey: 'AKIAKIAKI'
     secretkey: 'secret123'
     bucket: 'gitlab-registry-bucket-AKIAKIAKI'
+    region: 'your-s3-region'
   cache:
     blobdescriptor: inmemory
   delete:
     enabled: true
 ```
 
+## Change the registry's internal port
+
+> **Note:**
+This is not to be confused with the port that GitLab itself uses to expose
+the Registry to the world.
+
+The Registry server listens on localhost at port `5000` by default,
+which is the address for which the Registry server should accept connections.
+In the examples below we set the Registry's port to `5001`.
+
+**Omnibus GitLab**
+
+1. Open `/etc/gitlab/gitlab.rb` and set `registry['registry_http_addr']`:
+
+    ```ruby
+    registry['registry_http_addr'] = "localhost:5001"
+    ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open the configuration file of your Registry server and edit the
+   [`http:addr`][registry-http-config] value:
+
+    ```
+    http
+      addr: localhost:5001
+    ```
+
+1. Save the file and restart the Registry server.
+
+## Disable Container Registry per project
+
+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.
+
 ## Storage limitations
 
 Currently, there is no storage limitation, which means a user can upload an
@@ -453,6 +481,8 @@ configurable in future releases.
 [docker-insecure]: https://docs.docker.com/registry/insecure/
 [registry-deploy]: https://docs.docker.com/registry/deploying/
 [storage-config]: https://docs.docker.com/registry/configuration/#storage
+[registry-http-config]: https://docs.docker.com/registry/configuration/#http
+[registry-auth]: https://docs.docker.com/registry/configuration/#auth
 [token-config]: https://docs.docker.com/registry/configuration/#token
 [8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
 [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 7f53915a4d7c861b023bdf7410483172ee387467..b4a953d1ccc0a0aea42cf07335b463f9b4f6c1d3 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,15 +13,17 @@ override certain values.
 
 Variable | Type | Description
 -------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD`      | string | Sets the password for the `root` user on installation
-`GITLAB_HOST`               | string | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV`                 | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL`              | string | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM`         | string | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO`     | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD`        | string  | Sets the password for the `root` user on installation
+`GITLAB_HOST`                 | string  | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV`                   | string  | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL`                | string  | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM`           | string  | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME`   | string  | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO`       | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO`       | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX` | string  | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN`   | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX`   | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
 
 ## Complete database variables
 
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 8a881ce886303ff08ad783cdb87d72f8dd0be09d..137fed35a7398f48d830f9d9173fe2661e196627 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -101,9 +101,9 @@ need some additional configuration.
 
     ```ruby
     gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
-    gitlab_rails['secret_token'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
-    gitlab_ci['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
-    gitlab_ci['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
+    gitlab_rails['otp_key_base'] = 'b719fe119132c7810908bba18315259ed12888d4f5ee5430c42a776d840a396799b0a5ef0a801348c8a357f07aa72bbd58e25a84b8f247a25c72f539c7a6c5fa'
+    gitlab_rails['secret_key_base'] = '6e657410d57c71b4fc3ed0d694e7842b1895a8b401d812c17fe61caf95b48a6d703cb53c112bc01ebd197a85da81b18e29682040e99b4f26594772a4a2c98c6d'
+    gitlab_rails['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964'
     ```
 
 1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index 34b4f1faa94adfc9e3cb1b4b75cc148590d0113c..f846c06ca4224fa39a32c5b77563fe0b7c559ee4 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -3,6 +3,14 @@
 > [Introduced][ce-2371] in GitLab 8.4.
 
 ---
+## Automatic housekeeping
+
+GitLab automatically runs `git gc` and `git repack` on repositories
+after Git pushes. If needed you can change how often this happens, or
+to turn it off, go to **Admin area > Settings**
+(`/admin/application_settings`).
+
+## Manual housekeeping
 
 The housekeeping function runs `git gc` ([man page][man]) on the current
 project Git repository.
@@ -12,7 +20,7 @@ revisions (to reduce disk space and increase performance) and removing
 unreachable objects which may have been created from prior invocations of
 `git add`.
 
-You can find this option under your **[Project] > Settings**.
+You can find this option under your **[Project] > Edit Project**.
 
 ---
 
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
index f72ad9a45d5c16d31a63c3e4f8b4853b0d7dc281..6ebc6205635f81fcba372bd57b70c492f5c9b144 100644
Binary files a/doc/administration/img/housekeeping_settings.png and b/doc/administration/img/housekeeping_settings.png differ
diff --git a/doc/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png
similarity index 100%
rename from doc/raketasks/check_repos_output.png
rename to doc/administration/img/raketasks/check_repos_output.png
diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png
index 599350bc098052df2b51da8cb065f246463d8031..6481baca1ad90a76c3f09f554833a5fe9fb366d9 100644
Binary files a/doc/administration/img/repository_storages_admin_ui.png and b/doc/administration/img/repository_storages_admin_ui.png differ
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
new file mode 100644
index 0000000000000000000000000000000000000000..b95c425842ceeb14e64bea1ade6474f89fbe1601
--- /dev/null
+++ b/doc/administration/integration/koding.md
@@ -0,0 +1,243 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through installing and configuring Koding with
+GitLab.
+
+First of all, to be able to use Koding and GitLab together you will need public
+access to your server. This allows you to use single sign-on from GitLab to
+Koding and using vms from cloud providers like AWS. Koding has a registry for
+VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
+cloud providers register themselves to Kontrol via the agent that we put into
+provisioned VMs. This agent is called Klient and it provides Koding to access
+and manage the target machine.
+
+Kontrol and Klient are based on another technology called
+[Kite](https://github.com/koding/kite), that we have written at Koding. Which is a
+microservice framework that allows you to develop microservices easily.
+
+## Requirements
+
+### Hardware
+
+Minimum requirements are;
+
+  - 2 cores CPU
+  - 3G RAM
+  - 10G Storage
+
+If you plan to use AWS to install Koding it is recommended that you use at
+least a `c3.xlarge` instance.
+
+### Software
+
+  - [Git](https://git-scm.com)
+  - [Docker](https://www.docker.com)
+  - [docker-compose](https://www.docker.com/products/docker-compose)
+
+Koding can run on most of the UNIX based operating systems, since it's shipped
+as containerized with Docker support, it can work on any operating system that
+supports Docker.
+
+Required services are:
+
+- **PostgreSQL** - Kontrol and Service DB provider
+- **MongoDB**    - Main DB provider the application
+- **Redis**      - In memory DB used by both application and services
+- **RabbitMQ**   - Message Queue for both application and services
+
+which are also provided as a Docker container by Koding.
+
+
+## Getting Started with Development Versions
+
+
+### Koding
+
+You can run `docker-compose` environment for developing koding by
+executing commands in the following snippet.
+
+```bash
+git clone https://github.com/koding/koding.git
+cd koding
+docker-compose -f docker-compose-init.yml run init
+docker-compose up
+```
+
+This should start koding on `localhost:8090`.
+
+By default there is no team exists in Koding DB. You'll need to create a team
+called `gitlab` which is the default team name for GitLab integration in the
+configuration. To make things in order it's recommended to create the `gitlab`
+team first thing after setting up Koding.
+
+
+### GitLab
+
+To install GitLab to your environment for development purposes it's recommended
+to use GitLab Development Kit which you can get it from
+[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
+
+After all those steps, gitlab should be running on `localhost:3000`
+
+
+## Integration
+
+Integration includes following components;
+
+  - Single Sign On with OAuth from GitLab to Koding
+  - System Hook integration for handling GitLab events on Koding
+    (`project_created`, `user_joined` etc.)
+  - Service endpoints for importing/executing stacks from GitLab to Koding
+    (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
+
+As it's pointed out before, you will need public access to this machine that
+you've installed Koding and GitLab on. Better to use a domain but a static IP
+is also fine.
+
+For IP based installation you can use [xip.io](https://xip.io) service which is
+free and provides DNS resolution to IP based requests like following;
+
+  - 127.0.0.1.xip.io              -> resolves to 127.0.0.1
+  - foo.bar.baz.127.0.0.1.xip.io  -> resolves to 127.0.0.1
+  - and so on...
+
+As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+a running koding instance on `127.0.0.1` server will be handled as `foo` team
+requests.
+
+
+### GitLab Side
+
+You need to enable Koding integration from Settings under Admin Area. To do
+that login with an Admin account and do followings;
+
+ - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
+ - scroll to bottom of the page until Koding section
+ - check `Enable Koding` checkbox
+ - provide GitLab team page for running Koding instance as `Koding URL`*
+
+* For `Koding URL` you need to provide the gitlab integration enabled team on
+your Koding installation. Team called `gitlab` has integration on Koding out
+of the box, so if you didn't change anything your team on Koding should be
+`gitlab`.
+
+So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+with your Koding installation here.
+
+
+#### Registering Koding for OAuth integration
+
+We need `Application ID` and `Secret` to enable login to Koding via GitLab
+feature and to do that you need to register running Koding as a new application
+to your running GitLab application. Follow
+[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
+enable this integration.
+
+Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
+which again you need to _replace `127.0.0.1` with your instance public IP._
+
+Take a copy of `Application ID` and `Secret` that is generated by the GitLab
+application, we will need those on _Koding Part_ of this guide.
+
+
+#### Registering system hooks to Koding (optional)
+
+Koding can take actions based on the events generated by GitLab application.
+This feature is still in progress and only following events are processed by
+Koding at the moment;
+
+  - user_create
+  - user_destroy
+
+All system events are handled but not implemented on Koding side.
+
+To enable this feature you need to provide a `URL` and a `Secret Token` to your
+GitLab application. Open your admin area on your GitLab app from
+[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
+and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
+endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
+keep a copy of it, we will need it on _Koding Part_ of this guide.
+
+_(replace `127.0.0.1` with your instance public IP)_
+
+
+### Koding Part
+
+If you followed the steps in GitLab part we should have followings to enable
+Koding part integrations;
+
+  - `Application ID` and `Secret` for OAuth integration
+  - `Secret Token` for system hook integration
+  - Public address of running GitLab instance
+
+
+#### Start Koding with GitLab URL
+
+Now we need to configure Koding with all this information to get things ready.
+If it's already running please stop koding first.
+
+##### From command-line
+
+Replace followings with the ones you got from GitLab part of this guide;
+
+```bash
+cd koding
+docker-compose run                              \
+  --service-ports backend                       \
+  /opt/koding/scripts/bootstrap-container build \
+  --host=**YOUR_IP**.xip.io                     \
+  --gitlabHost=**GITLAB_IP**                    \
+  --gitlabPort=**GITLAB_PORT**                  \
+  --gitlabToken=**SECRET_TOKEN**                \
+  --gitlabAppId=**APPLICATION_ID**              \
+  --gitlabAppSecret=**SECRET**
+```
+
+##### By updating configuration
+
+Alternatively you can update `gitlab` section on
+`config/credentials.default.coffee` like following;
+
+```
+gitlab =
+  host: '**GITLAB_IP**'
+  port: '**GITLAB_PORT**'
+  applicationId: '**APPLICATION_ID**'
+  applicationSecret: '**SECRET**'
+  team: 'gitlab'
+  redirectUri: ''
+  systemHookToken: '**SECRET_TOKEN**'
+  hooksEnabled: yes
+```
+
+and start by only providing the `host`;
+
+```bash
+cd koding
+docker-compose run                              \
+  --service-ports backend                       \
+  /opt/koding/scripts/bootstrap-container build \
+  --host=**YOUR_IP**.xip.io                     \
+```
+
+#### Enable Single Sign On
+
+Once you restarted your Koding and logged in with your username and password
+you need to activate oauth authentication for your user. To do that
+
+ - Navigate to Dashboard on Koding from;
+   `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ - Scroll down to Integrations section
+ - Click on toggle to turn On integration in GitLab integration section
+
+This will redirect you to your GitLab instance and will ask your permission (
+if you are not logged in to GitLab at this point you will be redirected after
+login) once you accept you will be redirected to your Koding instance.
+
+From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
+screen in your Koding instance.
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
new file mode 100644
index 0000000000000000000000000000000000000000..28e1fd4e12e2d09b628a4a560a7634f392cdd125
--- /dev/null
+++ b/doc/administration/issue_closing_pattern.md
@@ -0,0 +1,49 @@
+# Issue closing pattern
+
+>**Note:**
+This is the administration documentation.
+There is a separate [user documentation] on issue closing pattern.
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+## Change the issue closing pattern
+
+In order to change the pattern you need to have access to the server that GitLab
+is installed on.
+
+The default pattern can be located in [gitlab.yml.example] under the
+"Automatic issue closing" section.
+
+> **Tip:**
+You are advised to use http://rubular.com to test the issue closing pattern.
+Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
+`#\d+` when testing your patterns, which matches only local issue references like `#123`.
+
+**For Omnibus installations**
+
+1. Open `/etc/gitlab/gitlab.rb` with your editor.
+1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+   expression of your liking:
+
+    ```ruby
+    gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+    ```
+1. [Reconfigure] GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Open `gitlab.yml` with your editor.
+1. Change the value of `issue_closing_pattern`:
+
+    ```yaml
+    issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+    ```
+
+1. [Restart] GitLab for the changes to take effect.
+
+[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: restart_gitlab.md#installations-from-source
+[user documentation]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 737b39db16ceb1966f6349b1a49af400104df660..d757a3c2a6661c6bd9b91e6a085eb541fe75f5c8 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -13,7 +13,8 @@ This guide talks about how to read and use these system log files.
 
 This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
 omnibus package or in `/home/git/gitlab/log/production.log` for
-installations from source.
+installations from source. (When Gitlab is running in an environment
+other than production, the corresponding logfile is shown here.)
 
 It contains information about all performed requests. You can see the
 URL and type of request, IP address and what exactly parts of code were
diff --git a/doc/administration/monitoring/performance/gitlab_configuration.md b/doc/administration/monitoring/performance/gitlab_configuration.md
new file mode 100644
index 0000000000000000000000000000000000000000..771584268d9106533765710e68154da4419b9566
--- /dev/null
+++ b/doc/administration/monitoring/performance/gitlab_configuration.md
@@ -0,0 +1,40 @@
+# GitLab Configuration
+
+GitLab Performance Monitoring is disabled by default. To enable it and change any of its
+settings, navigate to the Admin area in **Settings > Metrics**
+(`/admin/application_settings`).
+
+The minimum required settings you need to set are the InfluxDB host and port.
+Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
+changes.
+
+---
+
+![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
+
+---
+
+Finally, a restart of all GitLab processes is required for the changes to take
+effect:
+
+```bash
+# For Omnibus installations
+sudo gitlab-ctl restart
+
+# For installations from source
+sudo service gitlab restart
+```
+
+## Pending Migrations
+
+When any migrations are pending, the metrics are disabled until the migrations
+have been performed.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
new file mode 100644
index 0000000000000000000000000000000000000000..7947b0fedc4eeb19a385c569431523da7f4440d8
--- /dev/null
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,111 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need to
+set up the right retention policies and continuous queries. The easiest way of
+doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
+repository.
+
+To use this repository you must first clone it:
+
+```
+git clone https://gitlab.com/gitlab-org/influxdb-management.git
+cd influxdb-management
+```
+
+Next you must install the required dependencies:
+
+```
+gem install bundler
+bundle install
+```
+
+Now you must configure the repository by first copying `.env.example` to `.env`
+and then editing the `.env` file to contain the correct InfluxDB settings. Once
+configured you can simply run `bundle exec rake` and the InfluxDB database will
+be configured for you.
+
+For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+Alternatively you can automatically import all the dashboards into your Grafana
+instance. See the README of the [Grafana dashboards][grafana-dashboards]
+repository for more information on this process.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e34fad71ce6ef121704f909617c827e7f706967
Binary files /dev/null and b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png differ
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 0000000000000000000000000000000000000000..f97624365c70d63dd24ef54ef578882b0081403e
Binary files /dev/null and b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d50e4c88c206027caadd602a7505ad31f8252a6
Binary files /dev/null and b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa39a53acaeec2ac38f2d18f0cae31c58dd9c166
Binary files /dev/null and b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png differ
diff --git a/doc/administration/monitoring/performance/img/grafana_save_icon.png b/doc/administration/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..c740e33cd1c3bc105f247dcbec3d42a0a632dfd1
Binary files /dev/null and b/doc/administration/monitoring/performance/img/grafana_save_icon.png differ
diff --git a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..db396423e30d9c7705e6852c7fbede02c4ef67bc
Binary files /dev/null and b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png differ
diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png
new file mode 100644
index 0000000000000000000000000000000000000000..73e2fdcab679dc299607a24dcfdb97de8121dbaa
Binary files /dev/null and b/doc/administration/monitoring/performance/img/request_profile_result.png differ
diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png
new file mode 100644
index 0000000000000000000000000000000000000000..04d87567816aaf0aa174c3506556111fc1f99e4f
Binary files /dev/null and b/doc/administration/monitoring/performance/img/request_profiling_token.png differ
diff --git a/doc/administration/monitoring/performance/influxdb_configuration.md b/doc/administration/monitoring/performance/influxdb_configuration.md
new file mode 100644
index 0000000000000000000000000000000000000000..c30cd2950d861bed573cfde89b35366ace2b3b24
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_configuration.md
@@ -0,0 +1,193 @@
+# InfluxDB Configuration
+
+The default settings provided by [InfluxDB] are not sufficient for a high traffic
+GitLab environment. The settings discussed in this document are based on the
+settings GitLab uses for GitLab.com, depending on your own needs you may need to
+further adjust them.
+
+If you are intending to run InfluxDB on the same server as GitLab, make sure
+you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
+
+Unless you are going with a budget setup, it's advised to run it separately.
+
+## Requirements
+
+- InfluxDB 0.9.5 or newer
+- A fairly modern version of Linux
+- At least 4GB of RAM
+- At least 10GB of storage for InfluxDB data
+
+Note that the RAM and storage requirements can differ greatly depending on the
+amount of data received/stored. To limit the amount of stored data users can
+look into [InfluxDB Retention Policies][influxdb-retention].
+
+## Installation
+
+Installing InfluxDB is out of the scope of this document. Please refer to the
+[InfluxDB documentation].
+
+## InfluxDB Server Settings
+
+Since InfluxDB has many settings that users may wish to customize themselves
+(e.g. what port to run InfluxDB on), we'll only cover the essentials.
+
+The configuration file in question is usually located at
+`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
+InfluxDB needs to be restarted.
+
+### Storage Engine
+
+InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
+storage engine is available, called [TSM Tree]. All users **must** use the new
+`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
+upcoming InfluxDB releases.
+
+Make sure you have the following in your configuration file:
+
+```
+[data]
+  dir = "/var/lib/influxdb/data"
+  engine = "tsm1"
+```
+
+### Admin Panel
+
+Production environments should have the InfluxDB admin panel **disabled**. This
+feature can be disabled by adding the following to your InfluxDB configuration
+file:
+
+```
+[admin]
+  enabled = false
+```
+
+### HTTP
+
+HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
+thus it should be enabled. When enabling make sure to _also_ enable
+authentication:
+
+```
+[http]
+  enabled = true
+  auth-enabled = true
+```
+
+_**Note:** Before you enable authentication, you might want to [create an
+admin user](#create-a-new-admin-user)._
+
+### UDP
+
+GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
+UDP can be done using the following settings:
+
+```
+[[udp]]
+  enabled = true
+  bind-address = ":8089"
+  database = "gitlab"
+  batch-size = 1000
+  batch-pending = 5
+  batch-timeout = "1s"
+  read-buffer = 209715200
+```
+
+This does the following:
+
+1. Enable UDP and bind it to port 8089 for all addresses.
+2. Store any data received in the "gitlab" database.
+3. Define a batch of points to be 1000 points in size and allow a maximum of
+   5 batches _or_ flush them automatically after 1 second.
+4. Define a UDP read buffer size of 200 MB.
+
+One of the most important settings here is the UDP read buffer size as if this
+value is set too low, packets will be dropped. You must also make sure the OS
+buffer size is set to the same value, the default value is almost never enough.
+
+To set the OS buffer size to 200 MB, on Linux you can run the following command:
+
+```bash
+sysctl -w net.core.rmem_max=209715200
+```
+
+To make this permanent, add the following to `/etc/sysctl.conf` and restart the
+server:
+
+```bash
+net.core.rmem_max=209715200
+```
+
+It is **very important** to make sure the buffer sizes are large enough to
+handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
+buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
+traffic, users may be able to use a smaller buffer size, but we highly recommend
+using _at least_ 100 MB.
+
+When enabling UDP, users should take care to not expose the port to the public,
+as doing so will allow anybody to write data into your InfluxDB database (as
+[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
+whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
+allowing traffic from members of said VLAN.
+
+## Create a new admin user
+
+If you want to [enable authentication](#http), you might want to [create an
+admin user][influx-admin]:
+
+```
+influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
+```
+
+## Create the `gitlab` database
+
+Once you get InfluxDB up and running, you need to create a database for GitLab.
+Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
+before creating a database.
+
+_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
+[HTTP authentication](#http), remember to append the username (`-username <username>`)
+and password (`-password <password>`)  you set earlier to the commands below._
+
+Run the following command to create a database named `gitlab`:
+
+```bash
+influx -execute 'CREATE DATABASE gitlab'
+```
+
+The name **must** be `gitlab`, do not use any other name.
+
+Next, make sure that the database was successfully created:
+
+```bash
+influx -execute 'SHOW DATABASES'
+```
+
+The output should be similar to:
+
+```
+name: databases
+---------------
+name
+_internal
+gitlab
+```
+
+That's it! Now your GitLab instance should send data to InfluxDB.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
+[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
+[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
+[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
+[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
+[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
diff --git a/doc/administration/monitoring/performance/influxdb_schema.md b/doc/administration/monitoring/performance/influxdb_schema.md
new file mode 100644
index 0000000000000000000000000000000000000000..eff0e29f58d5e856d762d6cc815b73b09b5806c3
--- /dev/null
+++ b/doc/administration/monitoring/performance/influxdb_schema.md
@@ -0,0 +1,97 @@
+# InfluxDB Schema
+
+The following measurements are currently stored in InfluxDB:
+
+- `PROCESS_file_descriptors`
+- `PROCESS_gc_statistics`
+- `PROCESS_memory_usage`
+- `PROCESS_method_calls`
+- `PROCESS_object_counts`
+- `PROCESS_transactions`
+- `PROCESS_views`
+- `events`
+
+Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
+process type. In all series, any form of duration is stored in milliseconds.
+
+## PROCESS_file_descriptors
+
+This measurement contains the number of open file descriptors over time. The
+value field `value` contains the number of descriptors.
+
+## PROCESS_gc_statistics
+
+This measurement contains Ruby garbage collection statistics such as the amount
+of minor/major GC runs (relative to the last sampling interval), the time spent
+in garbage collection cycles, and all fields/values returned by `GC.stat`.
+
+## PROCESS_memory_usage
+
+This measurement contains the process' memory usage (in bytes) over time. The
+value field `value` contains the number of bytes.
+
+## PROCESS_method_calls
+
+This measurement contains the methods called during a transaction along with
+their duration, and a name of the transaction action that invoked the method (if
+available). The method call duration is stored in the value field `duration`,
+while the method name is stored in the tag `method`. The tag `action` contains
+the full name of the transaction action. Both the `method` and `action` fields
+are in the following format:
+
+```
+ClassName#method_name
+```
+
+For example, a method called by the `show` method in the `UsersController` class
+would have `action` set to `UsersController#show`.
+
+## PROCESS_object_counts
+
+This measurement is used to store retained Ruby objects (per class) and the
+amount of retained objects. The number of objects is stored in the `count` value
+field while the class name is stored in the `type` tag.
+
+## PROCESS_transactions
+
+This measurement is used to store basic transaction details such as the time it
+took to complete a transaction, how much time was spent in SQL queries, etc. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration`  | The total duration of the transaction |
+| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
+| `method_duration` | The total time spent in method calls |
+| `sql_duration` | The total time spent in SQL queries |
+| `view_duration` | The total time spent in views |
+
+## PROCESS_views
+
+This measurement is used to store view rendering timings for a transaction. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The rendering time of the view |
+| `view` | The path of the view, relative to the application's root directory |
+
+The `action` tag contains the action name of the transaction that rendered the
+view.
+
+## events
+
+This measurement is used to store generic events such as the number of Git
+pushes, Emails sent, etc. Each point in this measurement has a single value
+field called `count`. The value of this field is simply set to `1`. Each point
+also has at least one tag: `event`. This tag's value is set to the event name.
+Depending on the event type additional tags may be available as well.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
new file mode 100644
index 0000000000000000000000000000000000000000..79904916b7e2bdb08719bf1d3186e0cc337f6e6f
--- /dev/null
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -0,0 +1,65 @@
+# GitLab Performance Monitoring
+
+GitLab comes with its own application performance measuring system as of GitLab
+8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
+Community and Enterprise editions.
+
+Apart from this introduction, you are advised to read through the following
+documents in order to understand and properly configure GitLab Performance Monitoring:
+
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
+
+## Introduction to GitLab Performance Monitoring
+
+GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
+including (but not limited to):
+
+- The time it took to complete a transaction (a web request or Sidekiq job).
+- The time spent in running SQL queries and rendering HAML views.
+- The time spent executing (instrumented) Ruby methods.
+- Ruby object allocations, and retained objects in particular.
+- System statistics such as the process' memory usage and open file descriptors.
+- Ruby garbage collection statistics.
+
+Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
+data can be visualized using [Grafana][grafana] or any other application that
+supports reading data from InfluxDB. Alternatively data can be queried using the
+InfluxDB CLI.
+
+## Metric Types
+
+Two types of metrics are collected:
+
+1. Transaction specific metrics.
+1. Sampled metrics, collected at a certain interval in a separate thread.
+
+### Transaction Metrics
+
+Transaction metrics are metrics that can be associated with a single
+transaction. This includes statistics such as the transaction duration, timings
+of any executed SQL queries, time spent rendering HAML views, etc. These metrics
+are collected for every Rack request and Sidekiq job processed.
+
+### Sampled Metrics
+
+Sampled metrics are metrics that can't be associated with a single transaction.
+Examples include garbage collection statistics and retained Ruby objects. These
+metrics are collected at a regular interval. This interval is made up out of two
+parts:
+
+1. A user defined interval.
+1. A randomly generated offset added on top of the interval, the same offset
+   can't be used twice in a row.
+
+The actual interval can be anywhere between a half of the defined interval and a
+half above the interval. For example, for a user defined interval of 15 seconds
+the actual interval can be anywhere between 7.5 and 22.5. The interval is
+re-generated for every sampling run instead of being generated once and re-used
+for the duration of the process' lifetime.
+
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[grafana]: http://grafana.org/
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
new file mode 100644
index 0000000000000000000000000000000000000000..c358dfbead24094cae7b50f3d4065eda2eef9e67
--- /dev/null
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -0,0 +1,16 @@
+# Request Profiling
+
+## Procedure
+1. Grab the profiling token from `Monitoring > Requests Profiles` admin page
+(highlighted in a blue in the image below).
+![Profile token](img/request_profiling_token.png)
+1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools
+    * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension
+    * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension
+    * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`
+1. Once request is finished (which will take a little longer than usual), you can
+view the profiling output from `Monitoring > Requests Profiles` admin page.
+![Profiling output](img/request_profile_result.png)
+
+## Cleaning up
+Profiling output will be cleared out every day via a Sidekiq worker.
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
new file mode 100644
index 0000000000000000000000000000000000000000..4b582d16b647be048d42c8e4fd0f19c73db80de8
--- /dev/null
+++ b/doc/administration/operations.md
@@ -0,0 +1,6 @@
+# GitLab operations
+
+- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
+- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
+- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
+- [Moving repositories to a new location](operations/moving_repositories.md)
diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md
new file mode 100644
index 0000000000000000000000000000000000000000..93521e976d51e05204c34addbedc8cb0e4c514ab
--- /dev/null
+++ b/doc/administration/operations/cleaning_up_redis_sessions.md
@@ -0,0 +1,52 @@
+# Cleaning up stale Redis sessions
+
+Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
+Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
+you have been running a large GitLab server (thousands of users) since before
+GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
+database after you upgrade to GitLab 7.3. You can also perform a cleanup while
+still running GitLab 7.2 or older, but in that case new stale sessions will
+start building up again after you clean up.
+
+In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
+hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
+GitLab 7.3.0, the keys are
+prefixed with 'session:gitlab:', so they would look like
+'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
+remove the keys in the old format.
+
+First we define a shell function with the proper Redis connection details.
+
+```
+rcli() {
+  # This example works for Omnibus installations of GitLab 7.3 or newer. For an
+  # installation from source you will have to change the socket path and the
+  # path to redis-cli.
+  sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
+}
+
+# test the new shell function; the response should be PONG
+rcli ping
+```
+
+Now we do a search to see if there are any session keys in the old format for
+us to clean up.
+
+```
+# returns the number of old-format session keys in Redis
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
+```
+
+If the number is larger than zero, you can proceed to expire the keys from
+Redis. If the number is zero there is nothing to clean up.
+
+```
+# Tell Redis to expire each matched key after 600 seconds.
+rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
+# This will print '(integer) 1' for each key that gets expired.
+```
+
+Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
+background save interval) your Redis database will be compacted. If you are
+still using GitLab 7.2, users who are not clicking around in GitLab during the
+10 minute expiry window will be signed out of GitLab.
diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md
new file mode 100644
index 0000000000000000000000000000000000000000..54adb99386a48f326d5e270c2f4ab23bc531a7b5
--- /dev/null
+++ b/doc/administration/operations/moving_repositories.md
@@ -0,0 +1,180 @@
+# Moving repositories managed by GitLab
+
+Sometimes you need to move all repositories managed by GitLab to
+another filesystem or another server. In this document we will look
+at some of the ways you can copy all your repositories from
+`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
+
+We will look at three scenarios: the target directory is empty, the
+target directory contains an outdated copy of the repositories, and
+how to deal with thousands of repositories.
+
+**Each of the approaches we list can/will overwrite data in the
+target directory `/mnt/gitlab/repositories`. Do not mix up the
+source and the target.**
+
+## Target directory is empty: use a tar pipe
+
+If the target directory `/mnt/gitlab/repositories` is empty the
+simplest thing to do is to use a tar pipe.  This method has low
+overhead and tar is almost always already installed on your system.
+However, it is not possible to resume an interrupted tar pipe:  if
+that happens then all data must be copied again.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+  tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to see progress, replace `-xf` with `-xvf`.
+
+### Tar pipe to another server
+
+You can also use a tar pipe to copy data to another server. If your
+'git' user has SSH access to the newserver as 'git@newserver', you
+can pipe the data through SSH.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+  ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to compress the data before it goes over the network
+(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
+
+## The target directory contains an outdated copy of the repositories: use rsync
+
+If the target directory already contains a partial / outdated copy
+of the repositories it may be wasteful to copy all the data again
+with tar. In this scenario it is better to use rsync. This utility
+is either already installed on your system or easily installable
+via apt, yum etc.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+  /mnt/gitlab/repositories
+```
+
+The `/.` in the command above is very important, without it you can
+easily get the wrong directory structure in the target directory.
+If you want to see progress, replace `-a` with `-av`.
+
+### Single rsync to another server
+
+If the 'git' user on your source system has SSH access to the target
+server you can send the repositories over the network with rsync.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+  git@newserver:/mnt/gitlab/repositories
+```
+
+## Thousands of Git repositories: use one rsync per repository
+
+Every time you start an rsync job it has to inspect all files in
+the source directory, all files in the target directory, and then
+decide what files to copy or not. If the source or target directory
+has many contents this startup phase of rsync can become a burden
+for your GitLab server. In cases like this you can make rsync's
+life easier by dividing its work in smaller pieces, and sync one
+repository at a time.
+
+In addition to rsync we will use [GNU
+Parallel](http://www.gnu.org/software/parallel/). This utility is
+not included in GitLab so you need to install it yourself with apt
+or yum.  Also note that the GitLab scripts we used below were added
+in GitLab 8.1.
+
+** This process does not clean up repositories at the target location that no
+longer exist at the source. ** If you start using your GitLab instance with
+`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
+after switching to the new repository storage directory.
+
+### Parallel rsync for all repositories known to GitLab
+
+This will sync repositories with 10 rsync processes at a time. We keep
+track of progress so that the transfer can be restarted if necessary.
+
+First we create a new directory, owned by 'git', to hold transfer
+logs. We assume the directory is empty before we start the transfer
+procedure, and that we are the only ones writing files in it.
+
+```
+# Omnibus
+sudo mkdir /var/opt/gitlab/transfer-logs
+sudo chown git:git /var/opt/gitlab/transfer-logs
+
+# Source
+sudo -u git -H mkdir /home/git/transfer-logs
+```
+
+We seed the process with a list of the directories we want to copy.
+
+```
+# Omnibus
+sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
+```
+
+Now we can start the transfer. The command below is idempotent, and
+the number of jobs done by GNU Parallel should converge to zero. If it
+does not some repositories listed in all-repos-1234.txt may have been
+deleted/renamed before they could be copied.
+
+```
+# Omnibus
+sudo -u git sh -c '
+cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
+  /usr/bin/env JOBS=10 \
+  /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+    /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
+    /var/opt/gitlab/git-data/repositories \
+    /mnt/gitlab/repositories
+'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c '
+cat /home/git/transfer-logs/* | sort | uniq -u |\
+  /usr/bin/env JOBS=10 \
+  bin/parallel-rsync-repos \
+    /home/git/transfer-logs/success-$(date +%s).log \
+    /home/git/repositories \
+    /mnt/gitlab/repositories
+`
+```
+
+### Parallel rsync only for repositories with recent activity
+
+Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
+Then you might only want to sync repositories that were changed via GitLab
+_after_ that time. You can use the 'SINCE' variable to tell 'rake
+gitlab:list_repos' to only print repositories with recent activity.
+
+```
+# Omnibus
+sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+  sudo -u git \
+  /usr/bin/env JOBS=10 \
+  /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
+    success-$(date +%s).log \
+    /var/opt/gitlab/git-data/repositories \
+    /mnt/gitlab/repositories
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
+  sudo -u git -H \
+  /usr/bin/env JOBS=10 \
+  bin/parallel-rsync-repos \
+    success-$(date +%s).log \
+    /home/git/repositories \
+    /mnt/gitlab/repositories
+```
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
new file mode 100644
index 0000000000000000000000000000000000000000..b5e783489898ab379f85241fe028e1f6add7e211
--- /dev/null
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -0,0 +1,40 @@
+# Sidekiq MemoryKiller
+
+The GitLab Rails application code suffers from memory leaks. For web requests
+this problem is made manageable using
+[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
+restarts Unicorn worker processes in between requests when needed. The Sidekiq
+MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
+to process background jobs.
+
+Unlike unicorn-worker-killer, which is enabled by default for all GitLab
+installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
+_only_ for Omnibus packages. The reason for this is that the MemoryKiller
+relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
+installations from source do not all use Runit or an equivalent.
+
+With the default settings, the MemoryKiller will cause a Sidekiq restart no
+more often than once every 15 minutes, with the restart causing about one
+minute of delay for incoming background jobs.
+
+## Configuring the MemoryKiller
+
+The MemoryKiller is controlled using environment variables.
+
+- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
+  greater than 0, then after each Sidekiq job, the MemoryKiller will check the
+  RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
+  process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
+  delayed shutdown is triggered. The default value for Omnibus packages is set
+  [in the omnibus-gitlab
+  repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+  a shutdown is triggered, the Sidekiq process will keep working normally for
+  another 15 minutes.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
+  time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
+  Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
+  Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
+  restart Sidekiq.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
+  the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md
new file mode 100644
index 0000000000000000000000000000000000000000..bad61151bda1ff45f7fb7a57a7f18026047f7b83
--- /dev/null
+++ b/doc/administration/operations/unicorn.md
@@ -0,0 +1,86 @@
+# Understanding Unicorn and unicorn-worker-killer
+
+## Unicorn
+
+GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
+server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
+a daemon written in Ruby and C that can load and run a Ruby on Rails
+application; in our case the Rails application is GitLab Community Edition or
+GitLab Enterprise Edition.
+
+Unicorn has a multi-process architecture to make better use of available CPU
+cores (processes can run on different cores) and to have stronger fault
+tolerance (most failures stay isolated in only one process and cannot take down
+GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
+environment with the GitLab application code, and then spawns 'workers' which
+inherit this clean initial environment. The 'master' never handles any
+requests, that is left to the workers. The operating system network stack
+queues incoming requests and distributes them among the workers.
+
+In a perfect world, the master would spawn its pool of workers once, and then
+the workers handle incoming web requests one after another until the end of
+time. In reality, worker processes can crash or time out: if the master notices
+that a worker takes too long to handle a request it will terminate the worker
+process with SIGKILL ('kill -9'). No matter how the worker process ended, the
+master process will replace it with a new 'clean' process again. Unicorn is
+designed to be able to replace 'crashed' workers without dropping user
+requests.
+
+This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
+master process has PID 56227 below.
+
+```
+[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
+[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
+[2015-06-05T10:58:08.708141 #62538]  INFO -- : worker=10 spawned pid=62538
+[2015-06-05T10:58:08.708824 #62538]  INFO -- : worker=10 ready
+```
+
+### Tunables
+
+The main tunables for Unicorn are the number of worker processes and the
+request timeout after which the Unicorn master terminates a worker process.
+See the [omnibus-gitlab Unicorn settings
+documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
+if you want to adjust these settings.
+
+## unicorn-worker-killer
+
+GitLab has memory leaks. These memory leaks manifest themselves in long-running
+processes, such as Unicorn workers. (The Unicorn master process is not known to
+leak memory, probably because it does not handle user requests.)
+
+To make these memory leaks manageable, GitLab comes with the
+[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
+gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
+workers to do a memory self-check after every 16 requests. If the memory of the
+Unicorn worker exceeds a pre-set limit then the worker process exits. The
+Unicorn master then automatically replaces the worker process.
+
+This is a robust way to handle memory leaks: Unicorn is designed to handle
+workers that 'crash' so no user requests will be dropped. The
+unicorn-worker-killer gem is designed to only terminate a worker process _in
+between requests_, so no user requests are affected.
+
+This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
+You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
+The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
+threshold is a random value between 200 and 250 MB.  The master process (PID
+117565) then reaps the worker process and spawns a new 'worker 4' with PID
+127549.
+
+```
+[2015-06-05T12:07:41.828374 #125918]  WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
+[2015-06-05T12:07:41.828472 #125918]  WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
+[2015-06-05T12:07:42.025916 #117565]  INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
+[2015-06-05T12:07:42.034527 #127549]  INFO -- : worker=4 spawned pid=127549
+[2015-06-05T12:07:42.035217 #127549]  INFO -- : worker=4 ready
+```
+
+One other thing that stands out in the log snippet above, taken from
+GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
+is a normal value for our current GitLab.com setup and traffic.
+
+The high frequency of Unicorn memory restarts on some GitLab sites can be a
+source of confusion for administrators. Usually they are a [red
+herring](https://en.wikipedia.org/wiki/Red_herring).
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
new file mode 100644
index 0000000000000000000000000000000000000000..d1d2fed486126c43ddf39430a0af7977d78637f0
--- /dev/null
+++ b/doc/administration/raketasks/check.md
@@ -0,0 +1,97 @@
+# Check Rake Tasks
+
+## Repository Integrity
+
+Even though Git is very resilient and tries to prevent data integrity issues,
+there are times when things go wrong. The following Rake tasks intend to
+help GitLab administrators diagnose problem repositories so they can be fixed.
+
+There are 3 things that are checked to determine integrity.
+
+1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
+   This step verifies the connectivity and validity of objects in the repository.
+1. Check for `config.lock` in the repository directory.
+1. Check for any branch/references lock files in `refs/heads`.
+
+It's important to note that the existence of `config.lock` or reference locks
+alone do not necessarily indicate a problem. Lock files are routinely created
+and removed as Git and GitLab perform operations on the repository. They serve
+to prevent data integrity issues. However, if a Git operation is interrupted these
+locks may not be cleaned up properly.
+
+The following symptoms may indicate a problem with repository integrity. If users
+experience these symptoms you may use the rake tasks described below to determine
+exactly which repositories are causing the trouble.
+
+- Receiving an error when trying to push code - `remote: error: cannot lock ref`
+- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
+
+### Check all GitLab repositories
+
+This task loops through all repositories on the GitLab server and runs the
+3 integrity checks described previously.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:repo:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
+```
+
+### Check repositories for a specific user
+
+This task checks all repositories that a specific user has access to. This is important
+because sometimes you know which user is experiencing trouble but you don't know
+which project might be the cause.
+
+If the rake task is executed without brackets at the end, you will be prompted
+to enter a username.
+
+**Omnibus Installation**
+
+```bash
+sudo gitlab-rake gitlab:user:check_repos
+sudo gitlab-rake gitlab:user:check_repos[<username>]
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
+```
+
+Example output:
+
+![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png)
+
+## LDAP Check
+
+The LDAP check Rake task will test the bind_dn and password credentials
+(if configured) and will list a sample of LDAP users. This task is also
+executed as part of the `gitlab:check` task, but can run independently
+using the command below.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:ldap:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+```
+
+By default, the task will return a sample of 100 LDAP users. Change this
+limit by passing a number to the check task:
+
+```bash
+rake gitlab:ldap:check[50]
+```
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
new file mode 100644
index 0000000000000000000000000000000000000000..f3c2e72341f69db88b044a800671260a92381837
--- /dev/null
+++ b/doc/administration/raketasks/maintenance.md
@@ -0,0 +1,220 @@
+# Maintenance Rake Tasks
+
+## Gather information about GitLab and the system it runs on
+
+This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:env:info
+```
+
+**Source Installation**
+
+```
+bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+Example output:
+
+```
+System information
+System:           Debian 7.8
+Current User:     git
+Using RVM:        no
+Ruby Version:     2.1.5p273
+Gem Version:      2.4.3
+Bundler Version:  1.7.6
+Rake Version:     10.3.2
+Sidekiq Version:  2.17.8
+
+GitLab information
+Version:          7.7.1
+Revision:         41ab9e1
+Directory:        /home/git/gitlab
+DB Adapter:       postgresql
+URL:              https://gitlab.example.com
+HTTP Clone URL:   https://gitlab.example.com/some-project.git
+SSH Clone URL:    git@gitlab.example.com:some-project.git
+Using LDAP:       no
+Using Omniauth:   no
+
+GitLab Shell
+Version:          2.4.1
+Repositories:     /home/git/repositories/
+Hooks:            /home/git/gitlab-shell/hooks/
+Git:              /usr/bin/git
+```
+
+## Check GitLab configuration
+
+Runs the following rake tasks:
+
+- `gitlab:gitlab_shell:check`
+- `gitlab:sidekiq:check`
+- `gitlab:app:check`
+
+It will check that each component was setup according to the installation guide and suggest fixes for issues found.
+
+You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:check
+```
+
+**Source Installation**
+
+```
+bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
+
+Example output:
+
+```
+Checking Environment ...
+
+Git configured for git user? ... yes
+Has python2? ... yes
+python2 is supported version? ... yes
+
+Checking Environment ... Finished
+
+Checking GitLab Shell ...
+
+GitLab Shell version? ... OK (1.2.0)
+Repo base directory exists? ... yes
+Repo base directory is a symlink? ... no
+Repo base owned by git:git? ... yes
+Repo base access is drwxrws---? ... yes
+post-receive hook up-to-date? ... yes
+post-receive hooks in repos are links: ... yes
+
+Checking GitLab Shell ... Finished
+
+Checking Sidekiq ...
+
+Running? ... yes
+
+Checking Sidekiq ... Finished
+
+Checking GitLab ...
+
+Database config exists? ... yes
+Database is SQLite ... no
+All migrations up? ... yes
+GitLab config exists? ... yes
+GitLab config outdated? ... no
+Log directory writable? ... yes
+Tmp directory writable? ... yes
+Init script exists? ... yes
+Init script up-to-date? ... yes
+Redis version >= 2.0.0? ... yes
+
+Checking GitLab ... Finished
+```
+
+## Rebuild authorized_keys file
+
+In some case it is necessary to rebuild the `authorized_keys` file.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:shell:setup
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
+```
+
+```
+This will rebuild an authorized_keys file.
+You will lose any data stored in authorized_keys file.
+Do you want to continue (yes/no)? yes
+```
+
+## Clear redis cache
+
+If for some reason the dashboard shows wrong information you might want to
+clear Redis' cache.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake cache:clear
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+## Precompile the assets
+
+Sometimes during version upgrades you might end up with some wrong CSS or
+missing some icons. In that case, try to precompile the assets again.
+
+Note that this only applies to source installations and does NOT apply to
+Omnibus packages.
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+```
+
+For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
+the release of upstream GitLab. The omnibus version includes optimized versions
+of those assets. Unless you are modifying the JavaScript / CSS code on your
+production machine after installing the package, there should be no reason to redo
+rake assets:precompile on the production machine. If you suspect that assets
+have been corrupted, you should reinstall the omnibus package.
+
+## Tracking Deployments
+
+GitLab provides a Rake task that lets you track deployments in GitLab
+Performance Monitoring. This Rake task simply stores the current GitLab version
+in the GitLab Performance Monitoring database.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:track_deployment
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
+```
+
+## Create or repair repository hooks symlink
+
+If the GitLab shell hooks directory location changes or another circumstance
+leads to the hooks symlink becoming missing or invalid, run this Rake task
+to create or repair the symlinks.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:shell:create_hooks
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
+```
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
new file mode 100644
index 0000000000000000000000000000000000000000..5a9a158287741814e3ec1450290e2a5b23110570
--- /dev/null
+++ b/doc/administration/reply_by_email.md
@@ -0,0 +1,302 @@
+# Reply by email
+
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
+
+## Requirement
+
+Reply by email requires an IMAP-enabled email account. GitLab allows you to use
+three strategies for this feature:
+- using email sub-addressing
+- using a dedicated email address
+- using a catch-all mailbox
+
+### Email sub-addressing
+
+**If your provider or server supports email sub-addressing, we recommend using it.**
+
+[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
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
+mail server which you can run on-premises.
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+## How it works?
+
+### 1. GitLab sends a notification email
+
+When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
+header is set to the address defined in your GitLab configuration, with the
+`%{key}` placeholder (if present) replaced by a specific "reply key". In
+addition, this "reply key" is also added to the `References` header.
+
+### 2. You reply to the notification email
+
+When you reply to the notification email, your email client will:
+
+- send the email to the `Reply-To` address it got from the notification email
+- set the `In-Reply-To` header to the value of the `Message-ID` header from the
+  notification email
+- set the `References` header to the value of the `Message-ID` plus the value of
+  the notification email's `References` header.
+
+### 3. GitLab receives your reply to the notification email
+
+When GitLab receives your reply, it will look for the "reply key" in the
+following headers, in this order:
+
+1. the `To` header
+1. the `References` header
+
+If it finds a reply key, it will be able to leave your reply as a comment on
+the entity the notification was about (issue, merge request, commit...).
+
+For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
+please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
+
+## Set it up
+
+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).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
+[these instructions](./postfix.md).
+
+### Omnibus package installations
+
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+  feature and fill in the details for your specific IMAP server and email account:
+
+    ```ruby
+    # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+    gitlab_rails['incoming_email_enabled'] = true
+
+    # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+    # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+    gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
+
+    # Email account username
+    # With third party providers, this is usually the full email address.
+    # With self-hosted email servers, this is usually the user part of the email address.
+    gitlab_rails['incoming_email_email'] = "incoming"
+    # Email account password
+    gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+    # IMAP server host
+    gitlab_rails['incoming_email_host'] = "gitlab.example.com"
+    # IMAP server port
+    gitlab_rails['incoming_email_port'] = 143
+    # Whether the IMAP server uses SSL
+    gitlab_rails['incoming_email_ssl'] = false
+    # Whether the IMAP server uses StartTLS
+    gitlab_rails['incoming_email_start_tls'] = false
+
+    # The mailbox where incoming mail will end up. Usually "inbox".
+    gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+    ```
+
+    ```ruby
+    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+    gitlab_rails['incoming_email_enabled'] = true
+
+    # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+    # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+    gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+
+    # Email account username
+    # With third party providers, this is usually the full email address.
+    # With self-hosted email servers, this is usually the user part of the email address.
+    gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
+    # Email account password
+    gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+    # IMAP server host
+    gitlab_rails['incoming_email_host'] = "imap.gmail.com"
+    # IMAP server port
+    gitlab_rails['incoming_email_port'] = 993
+    # Whether the IMAP server uses SSL
+    gitlab_rails['incoming_email_ssl'] = true
+    # Whether the IMAP server uses StartTLS
+    gitlab_rails['incoming_email_start_tls'] = false
+
+    # The mailbox where incoming mail will end up. Usually "inbox".
+    gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+    ```
+
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
+
+    ```sh
+    sudo gitlab-ctl reconfigure
+    sudo gitlab-ctl restart mailroom
+    ```
+
+1. Verify that everything is configured correctly:
+
+    ```sh
+    sudo gitlab-rake gitlab:incoming_email:check
+    ```
+
+1. Reply by email should now be working.
+
+### Installations from source
+
+1. Go to the GitLab installation directory:
+
+    ```sh
+    cd /home/git/gitlab
+    ```
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+  and fill in the details for your specific IMAP server and email account:
+
+    ```sh
+    sudo editor config/gitlab.yml
+    ```
+
+    ```yaml
+    # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
+    incoming_email:
+      enabled: true
+
+      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+      address: "incoming+%{key}@gitlab.example.com"
+
+      # Email account username
+      # With third party providers, this is usually the full email address.
+      # With self-hosted email servers, this is usually the user part of the email address.
+      user: "incoming"
+      # Email account password
+      password: "[REDACTED]"
+
+      # IMAP server host
+      host: "gitlab.example.com"
+      # IMAP server port
+      port: 143
+      # Whether the IMAP server uses SSL
+      ssl: false
+      # Whether the IMAP server uses StartTLS
+      start_tls: false
+
+      # The mailbox where incoming mail will end up. Usually "inbox".
+      mailbox: "inbox"
+    ```
+
+    ```yaml
+    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+    incoming_email:
+      enabled: true
+
+      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+      address: "gitlab-incoming+%{key}@gmail.com"
+
+      # Email account username
+      # With third party providers, this is usually the full email address.
+      # With self-hosted email servers, this is usually the user part of the email address.
+      user: "gitlab-incoming@gmail.com"
+      # Email account password
+      password: "[REDACTED]"
+
+      # IMAP server host
+      host: "imap.gmail.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"
+    ```
+
+1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
+
+    ```sh
+    sudo mkdir -p /etc/default
+    echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
+    ```
+
+1. Restart GitLab:
+
+    ```sh
+    sudo service gitlab restart
+    ```
+
+1. Verify that everything is configured correctly:
+
+    ```sh
+    sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
+    ```
+
+1. Reply by email should now be working.
+
+### Development
+
+1. Go to the GitLab installation directory.
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
+
+    ```yaml
+    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+    incoming_email:
+      enabled: true
+
+      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+      address: "gitlab-incoming+%{key}@gmail.com"
+
+      # Email account username
+      # With third party providers, this is usually the full email address.
+      # With self-hosted email servers, this is usually the user part of the email address.
+      user: "gitlab-incoming@gmail.com"
+      # Email account password
+      password: "[REDACTED]"
+
+      # IMAP server host
+      host: "imap.gmail.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"
+    ```
+
+    As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+1. Uncomment the `mail_room` line in your `Procfile`:
+
+    ```yaml
+    mail_room: bundle exec mail_room -q -c config/mail_room.yml
+    ```
+
+1. Restart GitLab:
+
+    ```sh
+    bundle exec foreman start
+    ```
+
+1. Verify that everything is configured correctly:
+
+    ```sh
+    bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
+    ```
+
+1. Reply by email should now be working.
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
new file mode 100644
index 0000000000000000000000000000000000000000..22f10489a6c9f3eea901ec78883e9137a95da477
--- /dev/null
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -0,0 +1,324 @@
+# Set up Postfix for Reply by email
+
+This document will take you through the steps of setting up a basic Postfix mail
+server with IMAP authentication on Ubuntu, to be used with [Reply by email].
+
+The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
+
+## Configure your server firewall
+
+1. Open up port 25 on your server so that people can send email into the server over SMTP.
+2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+
+## Install packages
+
+1. Install the `postfix` package if it is not installed already:
+
+    ```sh
+    sudo apt-get install postfix
+    ```
+
+    When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
+
+1. Install the `mailutils` package.
+
+    ```sh
+    sudo apt-get install mailutils
+    ```
+
+## Create user
+
+1. Create a user for incoming email.
+
+    ```sh
+    sudo useradd -m -s /bin/bash incoming
+    ```
+
+1. Set a password for this user.
+
+    ```sh
+    sudo passwd incoming
+    ```
+
+    Be sure not to forget this, you'll need it later.
+
+## Test the out-of-the-box setup
+
+1. Connect to the local SMTP server:
+
+    ```sh
+    telnet localhost 25
+    ```
+
+    You should see a prompt like this:
+
+    ```sh
+    Trying 127.0.0.1...
+    Connected to localhost.
+    Escape character is '^]'.
+    220 gitlab.example.com ESMTP Postfix (Ubuntu)
+    ```
+
+    If you get a `Connection refused` error instead, verify that `postfix` is running:
+
+    ```sh
+    sudo postfix status
+    ```
+
+    If it is not, start it:
+
+    ```sh
+    sudo postfix start
+    ```
+
+1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+    ```
+    ehlo localhost
+    mail from: root@localhost
+    rcpt to: incoming@localhost
+    data
+    Subject: Re: Some issue
+
+    Sounds good!
+    .
+    quit
+    ```
+
+    _**Note:** The `.` is a literal period on its own line._
+
+    _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+    then your Postfix `my_network` configuration is not correct. The error will
+    say 'Temporary lookup failure'. See
+    [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
+
+1. Check if the `incoming` user received the email:
+
+    ```sh
+    su - incoming
+    mail
+    ```
+
+    You should see output like this:
+
+    ```
+    "/var/mail/incoming": 1 message 1 unread
+    >U   1 root@localhost                           59/2842  Re: Some issue
+    ```
+
+    Quit the mail app:
+
+    ```sh
+    q
+    ```
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+    ```sh
+    logout
+    ```
+
+## Configure Postfix to use Maildir-style mailboxes
+
+Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
+
+1. Configure Postfix to use Maildir-style mailboxes:
+
+    ```sh
+    sudo postconf -e "home_mailbox = Maildir/"
+    ```
+
+1. Restart Postfix:
+
+    ```sh
+    sudo /etc/init.d/postfix restart
+    ```
+
+1. Test the new setup:
+
+    1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
+    1. Check if the `incoming` user received the email:
+
+        ```sh
+        su - incoming
+        MAIL=/home/incoming/Maildir
+        mail
+        ```
+
+        You should see output like this:
+
+        ```
+        "/home/incoming/Maildir": 1 message 1 unread
+        >U   1 root@localhost                           59/2842  Re: Some issue
+        ```
+
+        Quit the mail app:
+
+        ```sh
+        q
+        ```
+
+    _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+    version of `mail` doesn't support Maildir style mailboxes. Install
+    `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+    try the above steps again, substituting `heirloom-mailx` for the `mail`
+    command._
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+    ```sh
+    logout
+    ```
+
+## Install the Courier IMAP server
+
+1. Install the `courier-imap` package:
+
+    ```sh
+    sudo apt-get install courier-imap
+    ```
+
+## Configure Postfix to receive email from the internet
+
+1. Let Postfix know about the domains that it should consider local:
+
+    ```sh
+    sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
+    ```
+
+1. Let Postfix know about the IPs that it should consider part of the LAN:
+
+    We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
+
+    ```sh
+    sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
+    ```
+
+1. Configure Postfix to receive mail on all interfaces, which includes the internet:
+
+    ```sh
+    sudo postconf -e "inet_interfaces = all"
+    ```
+
+1. Configure Postfix to use the `+` delimiter for sub-addressing:
+
+    ```sh
+    sudo postconf -e "recipient_delimiter = +"
+    ```
+
+1. Restart Postfix:
+
+    ```sh
+    sudo service postfix restart
+    ```
+
+## Test the final setup
+
+1. Test SMTP under the new setup:
+
+    1. Connect to the SMTP server:
+
+        ```sh
+        telnet gitlab.example.com 25
+        ```
+
+        You should see a prompt like this:
+
+        ```sh
+        Trying 123.123.123.123...
+        Connected to gitlab.example.com.
+        Escape character is '^]'.
+        220 gitlab.example.com ESMTP Postfix (Ubuntu)
+        ```
+
+        If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
+
+    1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+        ```
+        ehlo gitlab.example.com
+        mail from: root@gitlab.example.com
+        rcpt to: incoming@gitlab.example.com
+        data
+        Subject: Re: Some issue
+
+        Sounds good!
+        .
+        quit
+        ```
+
+        (Note: The `.` is a literal period on its own line)
+
+    1. Check if the `incoming` user received the email:
+
+        ```sh
+        su - incoming
+        MAIL=/home/incoming/Maildir
+        mail
+        ```
+
+        You should see output like this:
+
+        ```
+        "/home/incoming/Maildir": 1 message 1 unread
+        >U   1 root@gitlab.example.com                           59/2842  Re: Some issue
+        ```
+
+        Quit the mail app:
+
+        ```sh
+        q
+        ```
+
+    1. Log out of the `incoming` account and go back to being `root`:
+
+        ```sh
+        logout
+        ```
+
+1. Test IMAP under the new setup:
+
+    1. Connect to the IMAP server:
+
+        ```sh
+        telnet gitlab.example.com 143
+        ```
+
+        You should see a prompt like this:
+
+        ```sh
+        Trying 123.123.123.123...
+        Connected to mail.example.gitlab.com.
+        Escape character is '^]'.
+        - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc.  See COPYING for distribution information.
+        ```
+
+    1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
+
+        ```
+        a login incoming PASSWORD
+        ```
+
+        Replace PASSWORD with the password you set on the `incoming` user earlier.
+
+        You should see output like this:
+
+        ```
+        a OK LOGIN Ok.
+        ```
+
+    1. Disconnect from the IMAP server:
+
+        ```sh
+        a logout
+        ```
+
+## Done!
+
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+
+---
+
+_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+
+[reply by email]: reply_by_email.md
diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md
index 55b054fc1a440039384825e5698bccce1ea25743..ab70557b69adc7f3cbd7cc12a3551102615f5d5c 100644
--- a/doc/administration/repository_storages.md
+++ b/doc/administration/repository_storages.md
@@ -91,6 +91,9 @@ be stored via the **Application Settings** in the Admin area.
 
 ![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png)
 
+Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be
+randomly placed on one of the selected paths.
+
 [ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578
 [restart gitlab]: restart_gitlab.md#installations-from-source
 [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
index 483060395dd2fc996f90d54744f137a580a5cf03..b561c2f82aaac376e5c7254c0f1ce9ac1b93e633 100644
--- a/doc/administration/restart_gitlab.md
+++ b/doc/administration/restart_gitlab.md
@@ -139,7 +139,7 @@ If you are using other init systems, like systemd, you can check the
 
 [omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages"
 [install]: ../install/installation.md "Documentation to install GitLab from source"
-[mailroom]: ../incoming_email/README.md "Used for replying by email in GitLab issues and merge requests"
+[mailroom]: reply_by_email.md "Used for replying by email in GitLab issues and merge requests"
 [chef]: https://www.chef.io/chef/ "Chef official website"
 [src-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab "GitLab init service file"
 [gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository"
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index d127d7b85e5049c2213859b19204988a10ae2663..6f1356ddf8f3225d37bec47c68b394a6e155c043 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section.
 1. To see the current threads, run:
 
     ```
-    apply all thread bt
+    thread apply all bt
     ```
 
 1. Once you're done debugging with `gdb`, be sure to detach from the process and exit:
@@ -144,14 +144,14 @@ separate Rails process to debug the issue:
 1. Obtain the private token for your user (Profile Settings -> Account).
 1. Bring up the GitLab Rails console. For omnibus users, run:
 
-    ````
+    ```
     sudo gitlab-rails console
     ```
 
 1. At the Rails console, run:
 
     ```ruby
-    [1] pry(main)> app.get '<URL FROM STEP 1>/private_token?<TOKEN FROM STEP 2>'
+    [1] pry(main)> app.get '<URL FROM STEP 2>/?private_token=<TOKEN FROM STEP 3>'
     ```
 
     For example:
diff --git a/doc/api/README.md b/doc/api/README.md
index f3117815c7cce97387b1970bec66c3f20e6846c3..f65b934b9dbbf35e3792564df059fbd9bee75349 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -10,22 +10,29 @@ following locations:
 
 - [Award Emoji](award_emoji.md)
 - [Branches](branches.md)
+- [Broadcast Messages](broadcast_messages.md)
 - [Builds](builds.md)
-- [Build triggers](build_triggers.md)
+- [Build Triggers](build_triggers.md)
 - [Build Variables](build_variables.md)
 - [Commits](commits.md)
+- [Deployments](deployments.md)
 - [Deploy Keys](deploy_keys.md)
+- [Gitignores templates](templates/gitignores.md)
+- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
 - [Groups](groups.md)
 - [Group Access Requests](access_requests.md)
 - [Group Members](members.md)
 - [Issues](issues.md)
+- [Issue Boards](boards.md)
 - [Keys](keys.md)
 - [Labels](labels.md)
 - [Merge Requests](merge_requests.md)
 - [Milestones](milestones.md)
-- [Open source license templates](licenses.md)
+- [Open source license templates](templates/licenses.md)
 - [Namespaces](namespaces.md)
 - [Notes](notes.md) (comments)
+- [Notification settings](notification_settings.md)
+- [Pipelines](pipelines.md)
 - [Projects](projects.md) including setting Webhooks
 - [Project Access Requests](access_requests.md)
 - [Project Members](members.md)
@@ -39,8 +46,10 @@ following locations:
 - [Sidekiq metrics](sidekiq_metrics.md)
 - [System Hooks](system_hooks.md)
 - [Tags](tags.md)
-- [Users](users.md)
 - [Todos](todos.md)
+- [Users](users.md)
+- [Validate CI configuration](ci/lint.md)
+- [Version](version.md)
 
 ### Internal CI API
 
@@ -51,11 +60,12 @@ The following documentation is for the [internal CI API](ci/README.md):
 
 ## Authentication
 
-All API requests require authentication via a token. There are three types of tokens
-available: private tokens, OAuth 2 tokens, and personal access tokens.
+All API requests require authentication via a session cookie or token. There are
+three types of tokens available: private tokens, OAuth 2 tokens, and personal
+access tokens.
 
-If a token is invalid or omitted, an error message will be returned with
-status code `401`:
+If authentication information is invalid or omitted, an error message will be
+returned with status code `401`:
 
 ```json
 {
@@ -94,6 +104,13 @@ that needs access to the GitLab API.
 Once you have your token, pass it to the API using either the `private_token`
 parameter or the `PRIVATE-TOKEN` header.
 
+
+### Session Cookie
+
+When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
+
 ## Basic Usage
 
 API requests should be prefixed with `api` and the API version. The API version
@@ -342,6 +359,19 @@ follows:
 }
 ```
 
+## Unknown route
+
+When you try to access an API URL that does not exist you will receive 404 Not Found.
+
+```
+HTTP/1.1 404 Not Found
+Content-Type: application/json
+{
+    "error": "404 Not Found"
+}
+```
+
+
 ## Clients
 
 There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 72ec99b7c56f3f411cd58cace0130ba6c6c09a8f..06111f4ab671a4d73c7f6878500b4395a2793653 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,12 +1,13 @@
 # Award Emoji
 
-> [Introduced][ce-4575] in GitLab 8.9.
+> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
+
 
 An awarded emoji tells a thousand words, and can be awarded on issues, merge
-requests and notes/comments. Issues, merge requests and notes are further called
+requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called
 `awardables`.
 
-## Issues and merge requests
+## Issues, merge requests, and snippets
 
 ### List an awardable's award emoji
 
@@ -15,6 +16,7 @@ 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/snippets/:snippet_id/award_emoji
 ```
 
 Parameters:
@@ -41,7 +43,7 @@ Example Response:
       "id": 1,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "http://gitlab.example.com/u/root"
+      "web_url": "http://gitlab.example.com/root"
     },
     "created_at": "2016-06-15T10:09:34.206Z",
     "updated_at": "2016-06-15T10:09:34.206Z",
@@ -57,7 +59,7 @@ Example Response:
       "id": 26,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
-      "web_url": "http://gitlab.example.com/u/user4"
+      "web_url": "http://gitlab.example.com/user4"
     },
     "created_at": "2016-06-15T10:09:34.177Z",
     "updated_at": "2016-06-15T10:09:34.177Z",
@@ -69,11 +71,12 @@ Example Response:
 
 ### Get single award emoji
 
-Gets a single award emoji from an issue or merge request.
+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/snippets/:snippet_id/award_emoji/:award_id
 ```
 
 Parameters:
@@ -100,7 +103,7 @@ Example Response:
     "id": 26,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/user4"
+    "web_url": "http://gitlab.example.com/user4"
   },
   "created_at": "2016-06-15T10:09:34.177Z",
   "updated_at": "2016-06-15T10:09:34.177Z",
@@ -116,6 +119,7 @@ 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/snippets/:snippet_id/award_emoji
 ```
 
 Parameters:
@@ -142,7 +146,7 @@ Example Response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/root"
+    "web_url": "http://gitlab.example.com/root"
   },
   "created_at": "2016-06-17T17:47:29.266Z",
   "updated_at": "2016-06-17T17:47:29.266Z",
@@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz
 ```
 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/snippets/:snippet_id/award_emoji/:award_id
 ```
 
 Parameters:
@@ -185,7 +190,7 @@ Example Response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/root"
+    "web_url": "http://gitlab.example.com/root"
   },
   "created_at": "2016-06-17T17:47:29.266Z",
   "updated_at": "2016-06-17T17:47:29.266Z",
@@ -197,7 +202,7 @@ Example Response:
 ## Award Emoji on Notes
 
 The endpoints documented above are available for Notes as well. Notes
-are a sub-resource of Issues and Merge Requests. The examples below
+are a sub-resource of Issues, Merge Requests, or Snippets. The examples below
 describe working with Award Emoji on notes for an Issue, but can be
 easily adapted for notes on a Merge Request.
 
@@ -233,7 +238,7 @@ Example Response:
       "id": 26,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
-      "web_url": "http://gitlab.example.com/u/user4"
+      "web_url": "http://gitlab.example.com/user4"
     },
     "created_at": "2016-06-15T10:09:34.197Z",
     "updated_at": "2016-06-15T10:09:34.197Z",
@@ -274,7 +279,7 @@ Example Response:
     "id": 26,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/user4"
+    "web_url": "http://gitlab.example.com/user4"
   },
   "created_at": "2016-06-15T10:09:34.197Z",
   "updated_at": "2016-06-15T10:09:34.197Z",
@@ -314,7 +319,7 @@ Example Response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/root"
+    "web_url": "http://gitlab.example.com/root"
   },
   "created_at": "2016-06-17T19:59:55.888Z",
   "updated_at": "2016-06-17T19:59:55.888Z",
@@ -357,7 +362,7 @@ Example Response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://gitlab.example.com/u/root"
+    "web_url": "http://gitlab.example.com/root"
   },
   "created_at": "2016-06-17T19:59:55.888Z",
   "updated_at": "2016-06-17T19:59:55.888Z",
diff --git a/doc/api/boards.md b/doc/api/boards.md
new file mode 100644
index 0000000000000000000000000000000000000000..28681719f431256f479ce364477e3ddf6dbce8ec
--- /dev/null
+++ b/doc/api/boards.md
@@ -0,0 +1,251 @@
+# Boards
+
+Every API call to boards must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Project Board
+
+Lists Issue Boards in the given project.
+
+```
+GET /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`   | integer  | yes    | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+```
+
+Example response:
+
+```json
+[
+  {
+    "id" : 1,
+    "lists" : [
+      {
+        "id" : 1,
+        "label" : {
+          "name" : "Testing",
+          "color" : "#F0AD4E",
+          "description" : null
+        },
+        "position" : 1
+      },
+      {
+        "id" : 2,
+        "label" : {
+          "name" : "Ready",
+          "color" : "#FF0000",
+          "description" : null
+        },
+        "position" : 2
+      },
+      {
+        "id" : 3,
+        "label" : {
+          "name" : "Production",
+          "color" : "#FF5F00",
+          "description" : null
+        },
+        "position" : 3
+      }
+    ]
+  }
+]
+```
+
+## List board lists
+
+Get a list of the board's lists.
+Does not include `backlog` and `done` lists
+
+```
+GET /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`   | integer  | yes    | The ID of a project |
+| `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
+```
+
+Example response:
+
+```json
+[
+  {
+    "id" : 1,
+    "label" : {
+      "name" : "Testing",
+      "color" : "#F0AD4E",
+      "description" : null
+    },
+    "position" : 1
+  },
+  {
+    "id" : 2,
+    "label" : {
+      "name" : "Ready",
+      "color" : "#FF0000",
+      "description" : null
+    },
+    "position" : 2
+  },
+  {
+    "id" : 3,
+    "label" : {
+      "name" : "Production",
+      "color" : "#FF5F00",
+      "description" : null
+    },
+    "position" : 3
+  }
+]
+```
+
+## Single board list
+
+Get a single board list.
+
+```
+GET /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer | yes   | The ID of a project |
+| `board_id`   | integer  | yes    | The ID of a board |
+| `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
+```
+
+Example response:
+
+```json
+{
+  "id" : 1,
+  "label" : {
+    "name" : "Testing",
+    "color" : "#F0AD4E",
+    "description" : null
+  },
+  "position" : 1
+}
+```
+
+## New board list
+
+Creates a new Issue Board list.
+
+If the operation is successful, a status code of `200` and the newly-created
+list is returned. If an error occurs, an error number and a message explaining
+the reason is returned.
+
+```
+POST /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`            | integer | yes | The ID of a project |
+| `board_id`   | integer  | yes    | The ID of a board |
+| `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
+```
+
+Example response:
+
+```json
+{
+  "id" : 1,
+  "label" : {
+    "name" : "Testing",
+    "color" : "#F0AD4E",
+    "description" : null
+  },
+  "position" : 1
+}
+```
+
+## Edit board list
+
+Updates an existing Issue Board list. This call is used to change list position.
+
+If the operation is successful, a code of `200` and the updated board list is
+returned. If an error occurs, an error number and a message explaining the
+reason is returned.
+
+```
+PUT /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`            | integer | yes | The ID of a project |
+| `board_id`   | integer  | yes    | The ID of a board |
+| `list_id`      | integer | yes | The ID of a board's list |
+| `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
+```
+
+Example response:
+
+```json
+{
+  "id" : 1,
+  "label" : {
+    "name" : "Testing",
+    "color" : "#F0AD4E",
+    "description" : null
+  },
+  "position" : 1
+}
+```
+
+## Delete a board list
+
+Only for admins and project owners. Soft deletes the board list in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this board list, or it is not present, code `404` is given.
+
+```
+DELETE /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`            | integer | yes | The ID of a project |
+| `board_id`   | integer  | yes    | The ID of a board |
+| `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
+}
+```
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a9207a3ae51ecec296abe3162830cc8f19eee5
--- /dev/null
+++ b/doc/api/broadcast_messages.md
@@ -0,0 +1,158 @@
+# Broadcast Messages
+
+> **Note:** This feature was introduced in GitLab 8.12.
+
+The broadcast message API is only accessible to administrators. All requests by
+guests will respond with `401 Unauthorized`, and all requests by normal users
+will respond with `403 Forbidden`.
+
+## Get all broadcast messages
+
+```
+GET /broadcast_messages
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+```
+
+Example response:
+
+```json
+[
+    {
+        "message":"Example broadcast message",
+        "starts_at":"2016-08-24T23:21:16.078Z",
+        "ends_at":"2016-08-26T23:21:16.080Z",
+        "color":"#E75E40",
+        "font":"#FFFFFF",
+        "id":1,
+        "active": false
+    }
+]
+```
+
+## Get a specific broadcast message
+
+```
+GET /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `id`        | integer  | yes      | Broadcast message ID      |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+    "message":"Deploy in progress",
+    "starts_at":"2016-08-24T23:21:16.078Z",
+    "ends_at":"2016-08-26T23:21:16.080Z",
+    "color":"#cecece",
+    "font":"#FFFFFF",
+    "id":1,
+    "active":false
+}
+```
+
+## Create a broadcast message
+
+Responds with `400 Bad request` when the `message` parameter is missing or the
+`color` or `font` values are invalid, and `201 Created` when the broadcast
+message was successfully created.
+
+```
+POST /broadcast_messages
+```
+
+| Attribute   | Type     | Required | Description                                          |
+| ----------- | -------- | -------- | ---------------------------------------------------- |
+| `message`   | string   | yes      | Message to display                                   |
+| `starts_at` | datetime | no       | Starting time (defaults to current time)             |
+| `ends_at`   | datetime | no       | Ending time (defaults to one hour from current time) |
+| `color`     | string   | no       | Background color hex code                            |
+| `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
+```
+
+Example response:
+
+```json
+{
+    "message":"Deploy in progress",
+    "starts_at":"2016-08-26T00:41:35.060Z",
+    "ends_at":"2016-08-26T01:41:35.060Z",
+    "color":"#cecece",
+    "font":"#FFFFFF",
+    "id":1,
+    "active": true
+}
+```
+
+## Update a broadcast message
+
+```
+PUT /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `id`        | integer  | yes      | Broadcast message ID      |
+| `message`   | string   | no       | Message to display        |
+| `starts_at` | datetime | no       | Starting time             |
+| `ends_at`   | datetime | no       | Ending time               |
+| `color`     | string   | no       | Background color hex code |
+| `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
+```
+
+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
+}
+```
+
+## Delete a broadcast message
+
+```
+DELETE /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `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
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 8864df03c98c4efcecd2ee8e2fcc82c811478899..0476cac0edac35cf58cde63e5360af3f7eb59515 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -11,10 +11,10 @@ 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: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+| `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"
+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
@@ -40,6 +40,12 @@ Example of response
     "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",
@@ -58,7 +64,7 @@ Example of response
       "state": "active",
       "twitter": "",
       "username": "root",
-      "web_url": "http://gitlab.dev/u/root",
+      "web_url": "http://gitlab.dev/root",
       "website_url": ""
     }
   },
@@ -78,6 +84,12 @@ Example of response
     "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",
@@ -96,7 +108,7 @@ Example of response
       "state": "active",
       "twitter": "",
       "username": "root",
-      "web_url": "http://gitlab.dev/u/root",
+      "web_url": "http://gitlab.dev/root",
       "website_url": ""
     }
   }
@@ -120,10 +132,10 @@ GET /projects/:id/repository/commits/:sha/builds
 |-----------|---------|----------|---------------------|
 | `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: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+| `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"
+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
@@ -146,6 +158,12 @@ Example of response
     "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",
@@ -170,6 +188,12 @@ Example of response
     "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",
@@ -188,7 +212,7 @@ Example of response
       "state": "active",
       "twitter": "",
       "username": "root",
-      "web_url": "http://gitlab.dev/u/root",
+      "web_url": "http://gitlab.dev/root",
       "website_url": ""
     }
   }
@@ -231,6 +255,12 @@ Example of response
   "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",
@@ -249,7 +279,7 @@ Example of response
     "state": "active",
     "twitter": "",
     "username": "root",
-    "web_url": "http://gitlab.dev/u/root",
+    "web_url": "http://gitlab.dev/root",
     "website_url": ""
   }
 }
@@ -532,3 +562,49 @@ Example response:
   "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
+}
+```
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
index 2a71b087f193a091d5354c604b7cd6d0f5d1f692..b6d79706a843116d03ecf75b2041a624b742c316 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -38,6 +38,15 @@ POST /ci/api/v1/builds/register
 curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
 ```
 
+**Responses:**
+
+| Status | Data |Description                                                                |
+|--------|------|---------------------------------------------------------------------------|
+| `201`  | yes  | When a build is scheduled for a runner                                    |
+| `204`  | no   | When no builds are scheduled for a runner (for GitLab Runner >= `v1.3.0`) |
+| `403`  | no   | When invalid token is used or no token is sent                            |
+| `404`  | no   | When no builds are scheduled for a runner (for GitLab Runner < `v1.3.0`) **or** when the runner is set to `paused` in GitLab runner's configuration page |
+
 ### Update details of an existing build
 
 ```
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
new file mode 100644
index 0000000000000000000000000000000000000000..0c96b3ee335778cb1d7c11fb14d86098fde4aec4
--- /dev/null
+++ b/doc/api/ci/lint.md
@@ -0,0 +1,49 @@
+# Validate the .gitlab-ci.yml
+
+> [Introduced][ce-5953] in GitLab 8.12.
+
+Checks if your .gitlab-ci.yml file is valid.
+
+```
+POST ci/lint
+```
+
+| Attribute  | Type    | Required | Description |
+| ---------- | ------- | -------- | -------- |
+| `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\"]}}"}'
+```
+
+Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
+
+Example responses:
+
+* Valid content:
+
+    ```json
+    {
+      "status": "valid",
+      "errors": []
+    }
+    ```
+
+* Invalid content:
+
+    ```json
+    {
+      "status": "invalid",
+      "errors": [
+        "variables config should be a hash of key value pairs"
+      ]
+    }
+    ```
+
+* Without the content attribute:
+
+    ```json
+    {
+      "error": "content is missing"
+    }
+    ```
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index ecec53fde0371879031fd6b71a608f100f431c13..16028d1f124c79543b792722d4cc97b5944fab56 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -12,7 +12,9 @@ communication channel. For the consumer API see the
 This API uses two types of authentication:
 
 1. Unique Runner's token, which is the token assigned to the Runner after it
-   has been registered.
+   has been registered.  This token can be found on the Runner's edit page (go to
+   **Project > Runners**, select one of the Runners listed under **Runners activated for
+   this project**).
 
 2. Using Runners' registration token.
    This is a token that can be found in project's settings.
@@ -48,7 +50,7 @@ DELETE /ci/api/v1/runners/delete
 
 | Attribute | Type    | Required  | Description |
 | --------- | ------- | --------- | ----------- |
-| `token`   | string  | yes       | Runner's registration token |
+| `token`   | string  | yes       | Unique Runner's token |
 
 Example request:
 
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5c98c5d7565b5820a72af26bc05751b39152f900..e1ed99d98d379afd87d6f4027b6eaa2a752a0184 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
 | `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
 | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
@@ -46,6 +46,91 @@ Example response:
 ]
 ```
 
+## Create a commit with multiple files and actions
+
+> [Introduced][ce-6096] in GitLab 8.13.
+
+Create a commit by posting a JSON payload
+
+```
+POST /projects/:id/repository/commits
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
+| `branch_name` | string | yes | The name of a branch |
+| `commit_message` | string | yes | Commit message |
+| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
+| `author_email` | string | no | Specify the commit author's email address |
+| `author_name` | string | no | Specify the commit author's name |
+
+
+| `actions[]` Attribute | Type | Required | Description |
+| --------------------- | ---- | -------- | ----------- |
+| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update` |
+| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
+| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
+| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
+| `encoding` | string | no | `text` or `base64`. `text` is default. |
+
+```bash
+PAYLOAD=$(cat << 'JSON'
+{
+  "branch_name": "master",
+  "commit_message": "some commit message",
+  "actions": [
+    {
+      "action": "create",
+      "file_path": "foo/bar",
+      "content": "some content"
+    },
+    {
+      "action": "delete",
+      "file_path": "foo/bar2",
+    },
+    {
+      "action": "move",
+      "file_path": "foo/bar3",
+      "previous_path": "foo/bar4",
+      "content": "some content"
+    },
+    {
+      "action": "update",
+      "file_path": "foo/bar5",
+      "content": "new content"
+    }
+  ]
+}
+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
+```
+
+Example response:
+```json
+{
+  "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+  "short_id": "ed899a2f4b5",
+  "title": "some commit message",
+  "author_name": "Dmitriy Zaporozhets",
+  "author_email": "dzaporozhets@sphereconsultinginc.com",
+  "created_at": "2016-09-20T09:26:24.000-07:00",
+  "message": "some commit message",
+  "parent_ids": [
+    "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+  ],
+  "committed_date": "2016-09-20T09:26:24.000-07:00",
+  "authored_date": "2016-09-20T09:26:24.000-07:00",
+  "stats": {
+    "additions": 2,
+    "deletions": 2,
+    "total": 4
+  },
+  "status": null
+}
+```
+
 ## Get a single commit
 
 Get a specific commit identified by the commit hash or name of a branch or tag.
@@ -58,7 +143,7 @@ Parameters:
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
@@ -102,7 +187,7 @@ Parameters:
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
@@ -138,7 +223,7 @@ Parameters:
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha` | string | yes | The commit hash or name of a repository branch or tag |
 
 ```bash
@@ -187,7 +272,7 @@ POST /projects/:id/repository/commits/:sha/comments
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`        | integer | yes | The ID of a project |
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha`       | string  | yes | The commit SHA or name of a repository branch or tag |
 | `note`      | string  | yes | The text of the comment |
 | `path`      | string  | no  | The file path relative to the repository |
@@ -203,7 +288,7 @@ Example response:
 ```json
 {
    "author" : {
-      "web_url" : "https://gitlab.example.com/u/thedude",
+      "web_url" : "https://gitlab.example.com/thedude",
       "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
       "username" : "thedude",
       "state" : "active",
@@ -232,9 +317,9 @@ GET /projects/:id/repository/commits/:sha/statuses
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes | The ID of a project
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha`     | string  | yes | The commit SHA
-| `ref_name`| string  | no  | The name of a repository branch or tag or, if not given, the default branch
+| `ref`     | string  | no  | The name of a repository branch or tag or, if not given, the default branch
 | `stage`   | string  | no  | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
 | `name`    | string  | no  | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit`
 | `all`     | boolean | no  | Return all statuses, not only the latest ones
@@ -258,7 +343,7 @@ Example response:
       "author" : {
          "username" : "thedude",
          "state" : "active",
-         "web_url" : "https://gitlab.example.com/u/thedude",
+         "web_url" : "https://gitlab.example.com/thedude",
          "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
          "id" : 28,
          "name" : "Jeff Lebowski"
@@ -285,7 +370,7 @@ Example response:
          "id" : 28,
          "name" : "Jeff Lebowski",
          "username" : "thedude",
-         "web_url" : "https://gitlab.example.com/u/thedude",
+         "web_url" : "https://gitlab.example.com/thedude",
          "state" : "active",
          "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
       },
@@ -306,7 +391,7 @@ POST /projects/:id/statuses/:sha
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes   | The ID of a project
+| `id`      | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
 | `sha`     | string  | yes   | The commit SHA
 | `state`   | string  | yes   | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
 | `ref`     | string  | no    | The `ref` (branch or tag) to which the status refers
@@ -323,7 +408,7 @@ Example response:
 ```json
 {
    "author" : {
-      "web_url" : "https://gitlab.example.com/u/thedude",
+      "web_url" : "https://gitlab.example.com/thedude",
       "name" : "Jeff Lebowski",
       "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
       "username" : "thedude",
@@ -343,3 +428,5 @@ Example response:
    "finished_at" : "2016-01-19T09:05:50.365Z"
 }
 ```
+
+[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index b3bcf0dec4291fe891b190d81aea1de1754781c4..284d5f88c55e2b704dee9b3133236b556e9c0dca 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -2,7 +2,7 @@
 
 ## List all deploy keys
 
-Get a list of all deploy keys across all projects.
+Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires admin access.
 
 ```
 GET /deploy_keys
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
new file mode 100644
index 0000000000000000000000000000000000000000..3d95c4cde604add0089bddf52f8f5bab306ca983
--- /dev/null
+++ b/doc/api/deployments.md
@@ -0,0 +1,218 @@
+# Deployments API
+
+## List project deployments
+
+Get a list of deployments in a project.
+
+```
+GET /projects/:id/deployments
+```
+
+| Attribute | Type    | Required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
+```
+
+Example of response
+
+```json
+[
+  {
+    "created_at": "2016-08-11T07:36:40.222Z",
+    "deployable": {
+      "commit": {
+        "author_email": "admin@example.com",
+        "author_name": "Administrator",
+        "created_at": "2016-08-11T09:36:01.000+02:00",
+        "id": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+        "message": "Merge branch 'new-title' into 'master'\r\n\r\nUpdate README\r\n\r\n\r\n\r\nSee merge request !1",
+        "short_id": "99d03678",
+        "title": "Merge branch 'new-title' into 'master'\r"
+      },
+      "coverage": null,
+      "created_at": "2016-08-11T07:36:27.357Z",
+      "finished_at": "2016-08-11T07:36:39.851Z",
+      "id": 657,
+      "name": "deploy",
+      "ref": "master",
+      "runner": null,
+      "stage": "deploy",
+      "started_at": null,
+      "status": "success",
+      "tag": false,
+      "user": {
+        "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+        "bio": null,
+        "created_at": "2016-08-11T07:09:20.351Z",
+        "id": 1,
+        "is_admin": true,
+        "linkedin": "",
+        "location": null,
+        "name": "Administrator",
+        "skype": "",
+        "state": "active",
+        "twitter": "",
+        "username": "root",
+        "web_url": "http://localhost:3000/root",
+        "website_url": ""
+      }
+    },
+    "environment": {
+      "external_url": "https://about.gitlab.com",
+      "id": 9,
+      "name": "production"
+    },
+    "id": 41,
+    "iid": 1,
+    "ref": "master",
+    "sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "id": 1,
+      "name": "Administrator",
+      "state": "active",
+      "username": "root",
+      "web_url": "http://localhost:3000/root"
+    }
+  },
+  {
+    "created_at": "2016-08-11T11:32:35.444Z",
+    "deployable": {
+      "commit": {
+        "author_email": "admin@example.com",
+        "author_name": "Administrator",
+        "created_at": "2016-08-11T13:28:26.000+02:00",
+        "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+        "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
+        "short_id": "a91957a8",
+        "title": "Merge branch 'rename-readme' into 'master'\r"
+      },
+      "coverage": null,
+      "created_at": "2016-08-11T11:32:24.456Z",
+      "finished_at": "2016-08-11T11:32:35.145Z",
+      "id": 664,
+      "name": "deploy",
+      "ref": "master",
+      "runner": null,
+      "stage": "deploy",
+      "started_at": null,
+      "status": "success",
+      "tag": false,
+      "user": {
+        "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+        "bio": null,
+        "created_at": "2016-08-11T07:09:20.351Z",
+        "id": 1,
+        "is_admin": true,
+        "linkedin": "",
+        "location": null,
+        "name": "Administrator",
+        "skype": "",
+        "state": "active",
+        "twitter": "",
+        "username": "root",
+        "web_url": "http://localhost:3000/root",
+        "website_url": ""
+      }
+    },
+    "environment": {
+      "external_url": "https://about.gitlab.com",
+      "id": 9,
+      "name": "production"
+    },
+    "id": 42,
+    "iid": 2,
+    "ref": "master",
+    "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+    "user": {
+      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+      "id": 1,
+      "name": "Administrator",
+      "state": "active",
+      "username": "root",
+      "web_url": "http://localhost:3000/root"
+    }
+  }
+]
+```
+
+## Get a specific deployment
+
+```
+GET /projects/:id/deployments/:deployment_id
+```
+
+| Attribute | Type    | Required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+| `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"
+```
+
+Example of response
+
+```json
+{
+  "id": 42,
+  "iid": 2,
+  "ref": "master",
+  "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+  "created_at": "2016-08-11T11:32:35.444Z",
+  "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"
+  },
+  "environment": {
+    "id": 9,
+    "name": "production",
+    "external_url": "https://about.gitlab.com"
+  },
+  "deployable": {
+    "id": 664,
+    "status": "success",
+    "stage": "deploy",
+    "name": "deploy",
+    "ref": "master",
+    "tag": false,
+    "coverage": null,
+    "created_at": "2016-08-11T11:32:24.456Z",
+    "started_at": null,
+    "finished_at": "2016-08-11T11:32:35.145Z",
+    "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-11T07:09:20.351Z",
+      "is_admin": true,
+      "bio": null,
+      "location": null,
+      "skype": "",
+      "linkedin": "",
+      "twitter": "",
+      "website_url": ""
+    },
+    "commit": {
+      "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+      "short_id": "a91957a8",
+      "title": "Merge branch 'rename-readme' into 'master'\r",
+      "author_name": "Administrator",
+      "author_email": "admin@example.com",
+      "created_at": "2016-08-11T13:28:26.000+02:00",
+      "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
+    },
+    "runner": null
+  }
+}
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index a898387eaa2a6887ee7deedd3468462af8445b69..45a3118f27ab0e1925f78898547ce785c2d3543e 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -2,7 +2,12 @@
 
 ## List groups
 
-Get a list of groups. (As user: my groups, as admin: all groups)
+Get a list of groups. (As user: my groups or all available, as admin: all groups).
+
+Parameters:
+
+- `all_available` (optional) - if passed, show all groups you have access to
+- `skip_groups` (optional)(array of group IDs) - if passed, skip groups
 
 ```
 GET /groups
@@ -21,6 +26,14 @@ 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
+```
 
 ## List a group's projects
 
@@ -84,7 +97,8 @@ Parameters:
     "forks_count": 0,
     "open_issues_count": 3,
     "public_builds": true,
-    "shared_with_groups": []
+    "shared_with_groups": [],
+    "request_access_enabled": false
   }
 ]
 ```
@@ -118,6 +132,7 @@ Example response:
   "visibility_level": 20,
   "avatar_url": null,
   "web_url": "https://gitlab.example.com/groups/twitter",
+  "request_access_enabled": false,
   "projects": [
     {
       "id": 7,
@@ -163,7 +178,8 @@ Example response:
       "forks_count": 0,
       "open_issues_count": 3,
       "public_builds": true,
-      "shared_with_groups": []
+      "shared_with_groups": [],
+      "request_access_enabled": false
     },
     {
       "id": 6,
@@ -209,7 +225,8 @@ Example response:
       "forks_count": 0,
       "open_issues_count": 8,
       "public_builds": true,
-      "shared_with_groups": []
+      "shared_with_groups": [],
+      "request_access_enabled": false
     }
   ],
   "shared_projects": [
@@ -288,6 +305,8 @@ Parameters:
 - `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.
+- `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.
 
 ## Transfer project to group
 
@@ -317,6 +336,8 @@ PUT /groups/:id
 | `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. |
+| `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"
@@ -334,6 +355,7 @@ Example response:
   "visibility_level": 10,
   "avatar_url": null,
   "web_url": "http://gitlab.example.com/groups/h5bp",
+  "request_access_enabled": false,
   "projects": [
     {
       "id": 9,
@@ -378,7 +400,8 @@ Example response:
       "forks_count": 0,
       "open_issues_count": 3,
       "public_builds": true,
-      "shared_with_groups": []
+      "shared_with_groups": [],
+      "request_access_enabled": false
     }
   ]
 }
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a665645ad0ef618be6f51dc8d6b218891958693a..134263d27b4b1187621e9658b843255d5334288b 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -46,7 +46,7 @@ Example response:
       "author" : {
          "state" : "active",
          "id" : 18,
-         "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+         "web_url" : "https://gitlab.example.com/eileen.lowe",
          "name" : "Alexandra Bashirian",
          "avatar_url" : null,
          "username" : "eileen.lowe"
@@ -67,7 +67,7 @@ Example response:
          "state" : "active",
          "id" : 1,
          "name" : "Administrator",
-         "web_url" : "https://gitlab.example.com/u/root",
+         "web_url" : "https://gitlab.example.com/root",
          "avatar_url" : null,
          "username" : "root"
       },
@@ -79,7 +79,9 @@ Example response:
       "labels" : [],
       "subscribed" : false,
       "user_notes_count": 1,
-      "due_date": "2016-07-22"
+      "due_date": "2016-07-22",
+      "web_url": "http://example.com/example/example/issues/6",
+      "confidential": false
    }
 ]
 ```
@@ -132,7 +134,7 @@ Example response:
       },
       "author" : {
          "state" : "active",
-         "web_url" : "https://gitlab.example.com/u/root",
+         "web_url" : "https://gitlab.example.com/root",
          "avatar_url" : null,
          "username" : "root",
          "id" : 1,
@@ -143,7 +145,7 @@ Example response:
       "iid" : 1,
       "assignee" : {
          "avatar_url" : null,
-         "web_url" : "https://gitlab.example.com/u/lennie",
+         "web_url" : "https://gitlab.example.com/lennie",
          "state" : "active",
          "username" : "lennie",
          "id" : 9,
@@ -156,7 +158,9 @@ Example response:
       "created_at" : "2016-01-04T15:31:46.176Z",
       "subscribed" : false,
       "user_notes_count": 1,
-      "due_date": null
+      "due_date": null,
+      "web_url": "http://example.com/example/example/issues/1",
+      "confidential": false
    }
 ]
 ```
@@ -211,7 +215,7 @@ Example response:
       },
       "author" : {
          "state" : "active",
-         "web_url" : "https://gitlab.example.com/u/root",
+         "web_url" : "https://gitlab.example.com/root",
          "avatar_url" : null,
          "username" : "root",
          "id" : 1,
@@ -222,7 +226,7 @@ Example response:
       "iid" : 1,
       "assignee" : {
          "avatar_url" : null,
-         "web_url" : "https://gitlab.example.com/u/lennie",
+         "web_url" : "https://gitlab.example.com/lennie",
          "state" : "active",
          "username" : "lennie",
          "id" : 9,
@@ -235,7 +239,9 @@ Example response:
       "created_at" : "2016-01-04T15:31:46.176Z",
       "subscribed" : false,
       "user_notes_count": 1,
-      "due_date": "2016-07-22"
+      "due_date": "2016-07-22",
+      "web_url": "http://example.com/example/example/issues/1",
+      "confidential": false
    }
 ]
 ```
@@ -275,7 +281,7 @@ Example response:
    },
    "author" : {
       "state" : "active",
-      "web_url" : "https://gitlab.example.com/u/root",
+      "web_url" : "https://gitlab.example.com/root",
       "avatar_url" : null,
       "username" : "root",
       "id" : 1,
@@ -286,7 +292,7 @@ Example response:
    "iid" : 1,
    "assignee" : {
       "avatar_url" : null,
-      "web_url" : "https://gitlab.example.com/u/lennie",
+      "web_url" : "https://gitlab.example.com/lennie",
       "state" : "active",
       "username" : "lennie",
       "id" : 9,
@@ -299,7 +305,9 @@ Example response:
    "created_at" : "2016-01-04T15:31:46.176Z",
    "subscribed": false,
    "user_notes_count": 1,
-   "due_date": null
+   "due_date": null,
+   "web_url": "http://example.com/example/example/issues/1",
+   "confidential": false
 }
 ```
 
@@ -320,11 +328,12 @@ POST /projects/:id/issues
 | `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` |
-| `due_date`      | string  | no   | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `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` |
 
 ```bash
 curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -348,7 +357,7 @@ Example response:
       "name" : "Alexandra Bashirian",
       "avatar_url" : null,
       "state" : "active",
-      "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+      "web_url" : "https://gitlab.example.com/eileen.lowe",
       "id" : 18,
       "username" : "eileen.lowe"
    },
@@ -357,7 +366,9 @@ Example response:
    "milestone" : null,
    "subscribed" : true,
    "user_notes_count": 0,
-   "due_date": null
+   "due_date": null,
+   "web_url": "http://example.com/example/example/issues/14",
+   "confidential": false
 }
 ```
 
@@ -380,12 +391,13 @@ PUT /projects/:id/issues/:issue_id
 | `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` |
-| `due_date`      | string  | no   | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `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
@@ -402,7 +414,7 @@ Example response:
       "username" : "eileen.lowe",
       "id" : 18,
       "state" : "active",
-      "web_url" : "https://gitlab.example.com/u/eileen.lowe"
+      "web_url" : "https://gitlab.example.com/eileen.lowe"
    },
    "state" : "closed",
    "title" : "Issues with auth",
@@ -418,7 +430,9 @@ Example response:
    "milestone" : null,
    "subscribed" : true,
    "user_notes_count": 0,
-   "due_date": "2016-07-22"
+   "due_date": "2016-07-22",
+   "web_url": "http://example.com/example/example/issues/15",
+   "confidential": false
 }
 ```
 
@@ -486,7 +500,7 @@ Example response:
     "id": 12,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/axel.block"
+    "web_url": "https://gitlab.example.com/axel.block"
   },
   "author": {
     "name": "Kris Steuber",
@@ -494,9 +508,11 @@ Example response:
     "id": 10,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/solon.cremin"
+    "web_url": "https://gitlab.example.com/solon.cremin"
   },
-  "due_date": null
+  "due_date": null,
+  "web_url": "http://example.com/example/example/issues/11",
+  "confidential": false
 }
 ```
 
@@ -541,7 +557,7 @@ Example response:
     "id": 12,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/axel.block"
+    "web_url": "https://gitlab.example.com/axel.block"
   },
   "author": {
     "name": "Kris Steuber",
@@ -549,9 +565,11 @@ Example response:
     "id": 10,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/solon.cremin"
+    "web_url": "https://gitlab.example.com/solon.cremin"
   },
-  "due_date": null
+  "due_date": null,
+  "web_url": "http://example.com/example/example/issues/11",
+  "confidential": false
 }
 ```
 
@@ -596,7 +614,7 @@ Example response:
     "id": 21,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/keyon"
+    "web_url": "https://gitlab.example.com/keyon"
   },
   "author": {
     "name": "Vivian Hermann",
@@ -604,10 +622,12 @@ Example response:
     "id": 11,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/orville"
+    "web_url": "https://gitlab.example.com/orville"
   },
   "subscribed": false,
-  "due_date": null
+  "due_date": null,
+  "web_url": "http://example.com/example/example/issues/12",
+  "confidential": false
 }
 ```
 
@@ -649,7 +669,7 @@ Example response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/root"
+    "web_url": "https://gitlab.example.com/root"
   },
   "action_name": "marked",
   "target_type": "Issue",
@@ -680,7 +700,7 @@ Example response:
       "id": 14,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/francisca"
+      "web_url": "https://gitlab.example.com/francisca"
     },
     "author": {
       "name": "Maxie Medhurst",
@@ -688,12 +708,15 @@ Example response:
       "id": 12,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/craig_rutherford"
+      "web_url": "https://gitlab.example.com/craig_rutherford"
     },
     "subscribed": true,
     "user_notes_count": 7,
     "upvotes": 0,
-    "downvotes": 0
+    "downvotes": 0,
+    "due_date": null,
+    "web_url": "http://example.com/example/example/issues/110",
+    "confidential": false
   },
   "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
   "body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/keys.md b/doc/api/keys.md
index faa6f212b433a2ae9e1bb1b3984ff93d14ad2bef..b68f08a007d53dc81ffc82aca1de8aecddd46bbf 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -24,7 +24,7 @@ Parameters:
     "id": 25,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
-    "web_url": "http://localhost:3000/u/john_smith",
+    "web_url": "http://localhost:3000/john_smith",
     "created_at": "2015-09-03T07:24:01.670Z",
     "is_admin": false,
     "bio": null,
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 3653ccf304acf21220a37eeccd484bd4fddb40d8..78686fdcad4d13ee67ec6ae3182e42178de4d234 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -20,46 +20,61 @@ Example response:
 
 ```json
 [
-   {
-      "name" : "bug",
-      "color" : "#d9534f",
-      "description": "Bug reported by user",
-      "open_issues_count": 1,
-      "closed_issues_count": 0,
-      "open_merge_requests_count": 1
-   },
-   {
-      "color" : "#d9534f",
-      "name" : "confirmed",
-      "description": "Confirmed issue",
-      "open_issues_count": 2,
-      "closed_issues_count": 5,
-      "open_merge_requests_count": 0
-   },
-   {
-      "name" : "critical",
-      "color" : "#d9534f",
-      "description": "Critical issue. Need fix ASAP",
-      "open_issues_count": 1,
-      "closed_issues_count": 3,
-      "open_merge_requests_count": 1
-   },
-   {
-      "name" : "documentation",
-      "color" : "#f0ad4e",
-      "description": "Issue about documentation",
-      "open_issues_count": 1,
-      "closed_issues_count": 0,
-      "open_merge_requests_count": 2
-   },
-   {
-      "color" : "#5cb85c",
-      "name" : "enhancement",
-      "description": "Enhancement proposal",
-      "open_issues_count": 1,
-      "closed_issues_count": 0,
-      "open_merge_requests_count": 1
-   }
+  {
+    "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": 10
+  },
+  {
+    "id" : 4,
+    "color" : "#d9534f",
+    "name" : "confirmed",
+    "description": "Confirmed issue",
+    "open_issues_count": 2,
+    "closed_issues_count": 5,
+    "open_merge_requests_count": 0,
+    "subscribed": false,
+    "priority": null
+  },
+  {
+    "id" : 7,
+    "name" : "critical",
+    "color" : "#d9534f",
+    "description": "Critical issue. Need fix ASAP",
+    "open_issues_count": 1,
+    "closed_issues_count": 3,
+    "open_merge_requests_count": 1,
+    "subscribed": false,
+    "priority": null
+  },
+  {
+    "id" : 8,
+    "name" : "documentation",
+    "color" : "#f0ad4e",
+    "description": "Issue about documentation",
+    "open_issues_count": 1,
+    "closed_issues_count": 0,
+    "open_merge_requests_count": 2,
+    "subscribed": false,
+    "priority": null
+  },
+  {
+    "id" : 9,
+    "color" : "#5cb85c",
+    "name" : "enhancement",
+    "description": "Enhancement proposal",
+    "open_issues_count": 1,
+    "closed_issues_count": 0,
+    "open_merge_requests_count": 1,
+    "subscribed": true,
+    "priority": null
+  }
 ]
 ```
 
@@ -80,6 +95,7 @@ POST /projects/:id/labels
 | `name`        | string  | yes      | The name of the label        |
 | `color`       | string  | yes      | The color of the label in 6-digit hex notation with leading `#` sign |
 | `description` | string  | no       | The description of the label |
+| `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"
@@ -89,9 +105,15 @@ Example response:
 
 ```json
 {
-   "name" : "feature",
-   "color" : "#5843AD",
-   "description":null
+  "id" : 10,
+  "name" : "feature",
+  "color" : "#5843AD",
+  "description":null,
+  "open_issues_count": 0,
+  "closed_issues_count": 0,
+  "open_merge_requests_count": 0,
+  "subscribed": false,
+  "priority": null
 }
 ```
 
@@ -120,14 +142,15 @@ Example response:
 
 ```json
 {
-   "title" : "feature",
-   "color" : "#5843AD",
-   "description": "New feature proposal",
-   "updated_at" : "2015-11-03T21:22:30.737Z",
-   "template" : false,
-   "project_id" : 1,
-   "created_at" : "2015-11-03T21:22:30.737Z",
-   "id" : 9
+  "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
 }
 ```
 
@@ -148,9 +171,11 @@ PUT /projects/:id/labels
 | --------------- | ------- | --------------------------------- | -------------------------------  |
 | `id`            | integer | yes                               | The ID of the project            |
 | `name`          | string  | yes                               | The name of the existing label   |
-| `new_name`      | string  | yes if `color` if not provided    | The new name of the label        |
+| `new_name`      | string  | yes if `color` is not provided    | The new name of the label        |
 | `color`         | string  | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
 | `description`   | string  | no                                | The new description of the label |
+| `priority`    | integer | no       | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
+
 
 ```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"
@@ -160,9 +185,15 @@ Example response:
 
 ```json
 {
-   "color" : "#8E44AD",
-   "name" : "docs",
-   "description": "Documentation"
+  "id" : 8,
+  "name" : "docs",
+  "color" : "#8E44AD",
+  "description": "Documentation",
+  "open_issues_count": 1,
+  "closed_issues_count": 0,
+  "open_merge_requests_count": 2,
+  "subscribed": false,
+  "priority": null
 }
 ```
 
@@ -191,13 +222,15 @@ Example response:
 
 ```json
 {
-    "name": "Docs",
-    "color": "#cc0033",
-    "description": "",
-    "open_issues_count": 0,
-    "closed_issues_count": 0,
-    "open_merge_requests_count": 0,
-    "subscribed": true
+  "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": true,
+  "priority": null
 }
 ```
 
@@ -226,12 +259,14 @@ Example response:
 
 ```json
 {
-    "name": "Docs",
-    "color": "#cc0033",
-    "description": "",
-    "open_issues_count": 0,
-    "closed_issues_count": 0,
-    "open_merge_requests_count": 0,
-    "subscribed": false
+  "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
 }
 ```
diff --git a/doc/api/members.md b/doc/api/members.md
index d002e6eaf89ec27803ea01cb912ea77c1f8a0192..6535e9a7801df3afc35d8a8b6835d29388a86f91 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -86,7 +86,8 @@ Example response:
   "name": "Raymond Smith",
   "state": "active",
   "created_at": "2012-10-22T14:13:35Z",
-  "access_level": 30
+  "access_level": 30,
+  "expires_at": null
 }
 ```
 
@@ -106,10 +107,11 @@ POST /projects/:id/members
 | `id`      | integer/string  | yes | The group/project ID or path |
 | `user_id` | integer         | yes | The user ID of the new member |
 | `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
 
 ```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+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
 ```
 
 Example response:
@@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id
 | `id`      | integer/string | yes | The group/project ID or path |
 | `user_id` | integer | yes   | The user ID of the member |
 | `access_level` | integer | yes | A valid access level |
+| `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
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 3e88a7589365942c256c13bbbcaf4276ea41a25e..f4167403c2cd0d2ce11a69992a999b915d28213b 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -68,9 +68,12 @@ Parameters:
     "merge_when_build_succeeds": true,
     "merge_status": "can_be_merged",
     "subscribed" : false,
+    "sha": "8888888888888888888888888888888888888888",
+    "merge_commit_sha": null,
     "user_notes_count": 1,
     "should_remove_source_branch": true,
-    "force_remove_source_branch": false
+    "force_remove_source_branch": false,
+    "web_url": "http://example.com/example/example/merge_requests/1"
   }
 ]
 ```
@@ -134,9 +137,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": "9999999999999999999999999999999999999999",
   "user_notes_count": 1,
   "should_remove_source_branch": true,
-  "force_remove_source_branch": false
+  "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1"
 }
 ```
 
@@ -236,9 +242,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null,
   "user_notes_count": 1,
   "should_remove_source_branch": true,
   "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1",
   "changes": [
     {
     "old_path": "VERSION",
@@ -319,9 +328,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null,
   "user_notes_count": 0,
   "should_remove_source_branch": true,
-  "force_remove_source_branch": false
+  "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1"
 }
 ```
 
@@ -393,9 +405,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null,
   "user_notes_count": 1,
   "should_remove_source_branch": true,
-  "force_remove_source_branch": false
+  "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1"
 }
 ```
 
@@ -494,9 +509,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": "9999999999999999999999999999999999999999",
   "user_notes_count": 1,
   "should_remove_source_branch": true,
-  "force_remove_source_branch": false
+  "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1"
 }
 ```
 
@@ -563,9 +581,12 @@ Parameters:
   "merge_when_build_succeeds": true,
   "merge_status": "can_be_merged",
   "subscribed" : true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null,
   "user_notes_count": 1,
   "should_remove_source_branch": true,
-  "force_remove_source_branch": false
+  "force_remove_source_branch": false,
+  "web_url": "http://example.com/example/example/merge_requests/1"
 }
 ```
 
@@ -600,7 +621,7 @@ Example response when the GitLab issue tracker is used:
       "author" : {
          "state" : "active",
          "id" : 18,
-         "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+         "web_url" : "https://gitlab.example.com/eileen.lowe",
          "name" : "Alexandra Bashirian",
          "avatar_url" : null,
          "username" : "eileen.lowe"
@@ -621,7 +642,7 @@ Example response when the GitLab issue tracker is used:
          "state" : "active",
          "id" : 1,
          "name" : "Administrator",
-         "web_url" : "https://gitlab.example.com/u/root",
+         "web_url" : "https://gitlab.example.com/root",
          "avatar_url" : null,
          "username" : "root"
       },
@@ -690,7 +711,7 @@ Example response:
     "id": 19,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/leila"
+    "web_url": "https://gitlab.example.com/leila"
   },
   "assignee": {
     "name": "Celine Wehner",
@@ -698,7 +719,7 @@ Example response:
     "id": 16,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/carli"
+    "web_url": "https://gitlab.example.com/carli"
   },
   "source_project_id": 5,
   "target_project_id": 5,
@@ -717,7 +738,9 @@ Example response:
   },
   "merge_when_build_succeeds": false,
   "merge_status": "cannot_be_merged",
-  "subscribed": true
+  "subscribed": true,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null
 }
 ```
 
@@ -764,7 +787,7 @@ Example response:
     "id": 19,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/leila"
+    "web_url": "https://gitlab.example.com/leila"
   },
   "assignee": {
     "name": "Celine Wehner",
@@ -772,7 +795,7 @@ Example response:
     "id": 16,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/carli"
+    "web_url": "https://gitlab.example.com/carli"
   },
   "source_project_id": 5,
   "target_project_id": 5,
@@ -791,7 +814,9 @@ Example response:
   },
   "merge_when_build_succeeds": false,
   "merge_status": "cannot_be_merged",
-  "subscribed": false
+  "subscribed": false,
+  "sha": "8888888888888888888888888888888888888888",
+  "merge_commit_sha": null
 }
 ```
 
@@ -833,7 +858,7 @@ Example response:
     "id": 1,
     "state": "active",
     "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "https://gitlab.example.com/u/root"
+    "web_url": "https://gitlab.example.com/root"
   },
   "action_name": "marked",
   "target_type": "MergeRequest",
@@ -856,7 +881,7 @@ Example response:
       "id": 14,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/francisca"
+      "web_url": "https://gitlab.example.com/francisca"
     },
     "assignee": {
       "name": "Dr. Gabrielle Strosin",
@@ -864,7 +889,7 @@ Example response:
       "id": 4,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/barrett.krajcik"
+      "web_url": "https://gitlab.example.com/barrett.krajcik"
     },
     "source_project_id": 3,
     "target_project_id": 3,
@@ -884,9 +909,12 @@ Example response:
     "merge_when_build_succeeds": false,
     "merge_status": "unchecked",
     "subscribed": true,
+    "sha": "8888888888888888888888888888888888888888",
+    "merge_commit_sha": null,
     "user_notes_count": 7,
     "should_remove_source_branch": true,
-    "force_remove_source_branch": false
+    "force_remove_source_branch": false,
+    "web_url": "http://example.com/example/example/merge_requests/1"
   },
   "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
   "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
@@ -894,3 +922,112 @@ Example response:
   "created_at": "2016-07-01T11:14:15.530Z"
 }
 ```
+
+## Get MR diff versions
+
+Get a list of merge request diff versions.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions
+```
+
+| Attribute | Type    | Required | Description           |
+| --------- | ------- | -------- | --------------------- |
+| `id`      | String  | yes      | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+```
+
+Example response:
+
+```json
+[{
+  "id": 110,
+  "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+  "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "created_at": "2016-07-26T14:44:48.926Z",
+  "merge_request_id": 105,
+  "state": "collected",
+  "real_size": "1"
+}, {
+  "id": 108,
+  "head_commit_sha": "3eed087b29835c48015768f839d76e5ea8f07a24",
+  "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "created_at": "2016-07-25T14:21:33.028Z",
+  "merge_request_id": 105,
+  "state": "collected",
+  "real_size": "1"
+}]
+```
+
+## Get a single MR diff version
+
+Get a single merge request diff version.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/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 |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+```
+
+Example response:
+
+```json
+{
+  "id": 110,
+  "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+  "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+  "created_at": "2016-07-26T14:44:48.926Z",
+  "merge_request_id": 105,
+  "state": "collected",
+  "real_size": "1",
+  "commits": [{
+    "id": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+    "short_id": "33e2ee85",
+    "title": "Change year to 2018",
+    "author_name": "Administrator",
+    "author_email": "admin@example.com",
+    "created_at": "2016-07-26T17:44:29.000+03:00",
+    "message": "Change year to 2018"
+  }, {
+    "id": "aa24655de48b36335556ac8a3cd8bb521f977cbd",
+    "short_id": "aa24655d",
+    "title": "Update LICENSE",
+    "author_name": "Administrator",
+    "author_email": "admin@example.com",
+    "created_at": "2016-07-25T17:21:53.000+03:00",
+    "message": "Update LICENSE"
+  }, {
+    "id": "3eed087b29835c48015768f839d76e5ea8f07a24",
+    "short_id": "3eed087b",
+    "title": "Add license",
+    "author_name": "Administrator",
+    "author_email": "admin@example.com",
+    "created_at": "2016-07-25T17:21:20.000+03:00",
+    "message": "Add license"
+  }],
+  "diffs": [{
+    "old_path": "LICENSE",
+    "new_path": "LICENSE",
+    "a_mode": "0",
+    "b_mode": "100644",
+    "diff": "--- /dev/null\n+++ b/LICENSE\n@@ -0,0 +1,21 @@\n+The MIT License (MIT)\n+\n+Copyright (c) 2018 Administrator\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n",
+    "new_file": true,
+    "renamed_file": false,
+    "deleted_file": false
+  }]
+}
+```
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d140d06acfe5818232df1624c7f2802238bd71..58d40eecf3e3b33390d89e531713f6a3c8e16a05 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -78,7 +78,8 @@ Parameters:
 
 ### Create new issue note
 
-Creates a new note to a single project issue.
+Creates a new note to a single project issue. If you create a note where the body
+only contains an Award Emoji, you'll receive this object back.
 
 ```
 POST /projects/:id/issues/:issue_id/notes
@@ -142,7 +143,7 @@ Example Response:
     "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/u/pipin"
+    "web_url": "https://gitlab.example.com/pipin"
   },
   "created_at": "2016-04-05T22:10:44.164Z",
   "system": false,
@@ -204,6 +205,7 @@ Parameters:
 ### Create new snippet note
 
 Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
 
 ```
 POST /projects/:id/snippets/:snippet_id/notes
@@ -266,7 +268,7 @@ Example Response:
     "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/u/pipin"
+    "web_url": "https://gitlab.example.com/pipin"
   },
   "created_at": "2016-04-06T16:51:53.239Z",
   "system": false,
@@ -332,6 +334,8 @@ Parameters:
 ### Create new merge request note
 
 Creates a new note for a single merge request.
+If you create a note where the body only contains an Award Emoji, you'll receive
+this object back.
 
 ```
 POST /projects/:id/merge_requests/:merge_request_id/notes
@@ -394,7 +398,7 @@ Example Response:
     "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/u/pipin"
+    "web_url": "https://gitlab.example.com/pipin"
   },
   "created_at": "2016-04-05T22:11:59.923Z",
   "system": false,
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
new file mode 100644
index 0000000000000000000000000000000000000000..aea1c12a3927678fe6313376753ad870921499c8
--- /dev/null
+++ b/doc/api/notification_settings.md
@@ -0,0 +1,177 @@
+# Notification settings
+
+>**Note:** This feature was [introduced][ce-5632] in GitLab 8.12.
+
+**Valid notification levels**
+
+The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
+
+```
+disabled
+participating
+watch
+global
+mention
+custom
+```
+
+If the `custom` level is used, specific email events can be controlled. Notification email events are defined in the `NotificationSetting::EMAIL_EVENTS` model variable. Currently, these events are recognized:
+
+```
+new_note
+new_issue
+reopen_issue
+close_issue
+reassign_issue
+new_merge_request
+reopen_merge_request
+close_merge_request
+reassign_merge_request
+merge_merge_request
+failed_pipeline
+success_pipeline
+```
+
+## Global notification settings
+
+Get current notification settings and email address.
+
+```
+GET /notification_settings
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings
+```
+
+Example response:
+
+```json
+{
+  "level": "participating",
+  "notification_email": "admin@example.com"
+}
+```
+
+## Update global notification settings
+
+Update current notification settings and email address.
+
+```
+PUT /notification_settings
+```
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `level` | string | no | The global notification level |
+| `notification_email` | string | no | The email address to send notifications |
+| `new_note` | boolean | no | Enable/disable this notification |
+| `new_issue` | boolean | no | Enable/disable this notification |
+| `reopen_issue` | boolean | no | Enable/disable this notification |
+| `close_issue` | boolean | no | Enable/disable this notification |
+| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `reopen_merge_request` | boolean | no | Enable/disable this notification |
+| `close_merge_request` | boolean | no | Enable/disable this notification |
+| `reassign_merge_request` | boolean | no | Enable/disable this notification |
+| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
+
+Example response:
+
+```json
+{
+  "level": "watch",
+  "notification_email": "admin@example.com"
+}
+```
+
+## Group / project level notification settings
+
+Get current group or project notification settings.
+
+```
+GET /groups/:id/notification_settings
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+
+Example response:
+
+```json
+{
+  "level": "global"
+}
+```
+
+## Update group/project level notification settings
+
+Update current group/project notification settings.
+
+```
+PUT /groups/:id/notification_settings
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `level` | string | no | The global notification level |
+| `new_note` | boolean | no | Enable/disable this notification |
+| `new_issue` | boolean | no | Enable/disable this notification |
+| `reopen_issue` | boolean | no | Enable/disable this notification |
+| `close_issue` | boolean | no | Enable/disable this notification |
+| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `reopen_merge_request` | boolean | no | Enable/disable this notification |
+| `close_merge_request` | boolean | no | Enable/disable this notification |
+| `reassign_merge_request` | boolean | no | Enable/disable this notification |
+| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
+
+Example responses:
+
+```json
+{
+  "level": "watch"
+}
+
+{
+  "level": "custom",
+  "events": {
+    "new_note": true,
+    "new_issue": false,
+    "reopen_issue": false,
+    "close_issue": false,
+    "reassign_issue": false,
+    "new_merge_request": false,
+    "reopen_merge_request": false,
+    "close_merge_request": false,
+    "reassign_merge_request": false,
+    "merge_merge_request": false,
+    "failed_pipeline": false,
+    "success_pipeline": false
+  }
+}
+```
+
+[ce-5632]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 16ef79617c04c0ab974d7ec3ce6dd9181b3b16e0..5ef5e3f57447c6afd380e78425a8f6f1aa81a41c 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,10 +1,10 @@
-# GitLab as an OAuth2 client
+# GitLab as an OAuth2 provider
 
 This document covers using the OAuth2 protocol to access GitLab.
 
 If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
 
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. 
+OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
 
 This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
 
@@ -22,7 +22,7 @@ In the following sections you will be introduced to the three steps needed for t
 ### 1. Registering the client
 
 First, you should create an application (`/profile/applications`) in your user's account.
-Each application gets a unique App ID and App Secret parameters. 
+Each application gets a unique App ID and App Secret parameters.
 
 >**Note:**
 **You should not share/leak your App ID or App Secret.**
@@ -46,10 +46,10 @@ http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
 You should then use the `code` to request an access token.
 
 >**Important:**
-It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and 
-validate that value is returned and matches in the redirect request. 
-This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow), 
-`state` really should have been a requirement in the standard! 
+It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
+validate that value is returned and matches in the redirect request.
+This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
+`state` really should have been a requirement in the standard!
 
 ### 3. Requesting the access token
 
@@ -62,7 +62,7 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
 # The response will be
 {
  "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
- "token_type": "bearer", 
+ "token_type": "bearer",
  "expires_in": 7200,
  "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
 }
@@ -90,12 +90,12 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/
 
 ## Deprecation Notice
 
-1. Starting in GitLab 9.0, the Resource Owner Password Credentials will be *disabled* for users with two-factor authentication turned on.
+1. Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication turned on.
 2. These users can access the API using [personal access tokens] instead.
 
 ---
 
-In this flow, a token is requested in exchange for the resource owner credentials (username and password). 
+In this flow, a token is requested in exchange for the resource owner credentials (username and password).
 The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
 client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
 available (such as an authorization code).
@@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters:
 {
   "grant_type"    : "password",
   "username"      : "user@example.com",
-  "password"      : "sekret"
+  "password"      : "secret"
 }
 ```
 
@@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem:
 
 ```
 client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com")
-access_token = client.password.get_token('user@example.com', 'sekret')
+access_token = client.password.get_token('user@example.com', 'secret')
 puts access_token.token
 ```
 
-[personal access tokens]: ./README.md#personal-access-tokens
+[personal access tokens]: ./README.md#personal-access-tokens
\ No newline at end of file
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
new file mode 100644
index 0000000000000000000000000000000000000000..a29b3eb6f44527a8de0e206fb5c276cbab5dc917
--- /dev/null
+++ b/doc/api/pipelines.md
@@ -0,0 +1,207 @@
+# Pipelines API
+
+## List project pipelines
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines
+```
+
+| 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/pipelines"
+```
+
+Example of response
+
+```json
+[
+  {
+    "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
+  },
+  {
+    "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
+  }
+]
+```
+
+## Get a single pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines/:pipeline_id
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `pipeline_id` | integer | yes      | The ID of a pipeline   |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
+```
+
+Example of response
+
+```json
+{
+  "id": 46,
+  "status": "success",
+  "ref": "master",
+  "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-11T11:28:34.085Z",
+  "updated_at": "2016-08-11T11:32:35.169Z",
+  "started_at": null,
+  "finished_at": "2016-08-11T11:32:35.145Z",
+  "committed_at": null,
+  "duration": null
+}
+```
+
+## Retry failed builds in a pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/retry
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `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"
+```
+
+Response:
+
+```json
+{
+  "id": 46,
+  "status": "pending",
+  "ref": "master",
+  "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-11T11:28:34.085Z",
+  "updated_at": "2016-08-11T11:32:35.169Z",
+  "started_at": null,
+  "finished_at": "2016-08-11T11:32:35.145Z",
+  "committed_at": null,
+  "duration": null
+}
+```
+
+## Cancel a pipelines builds
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/cancel
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer | yes      | The ID of a project |
+| `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"
+```
+
+Response:
+
+```json
+{
+  "id": 46,
+  "status": "canceled",
+  "ref": "master",
+  "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-11T11:28:34.085Z",
+  "updated_at": "2016-08-11T11:32:35.169Z",
+  "started_at": null,
+  "finished_at": "2016-08-11T11:32:35.145Z",
+  "committed_at": null,
+  "duration": null
+}
+```
+
+[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index a7acf37b5bcc05b9e81fae2e1849b7d39607e4cf..c6685f54a9d297d26499754524d85b0b6db4061a 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -53,7 +53,8 @@ Parameters:
   },
   "expires_at": null,
   "updated_at": "2012-06-28T10:52:04Z",
-  "created_at": "2012-06-28T10:52:04Z"
+  "created_at": "2012-06-28T10:52:04Z",
+  "web_url": "http://example.com/example/example/snippets/1"
 }
 ```
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 37d97b2db44691b66a02b54b2aa090c7bd9e92ab..bbb3bfb49950cdcd244c8a52fc7af07344eb0fcb 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -20,7 +20,7 @@ Constants for project visibility levels are next:
 
 ## List projects
 
-Get a list of projects accessible by the authenticated user.
+Get a list of projects for which the authenticated user is a member.
 
 ```
 GET /projects
@@ -28,11 +28,152 @@ GET /projects
 
 Parameters:
 
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `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 |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+```json
+[
+  {
+    "id": 4,
+    "description": null,
+    "default_branch": "master",
+    "public": false,
+    "visibility_level": 0,
+    "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",
+    "tag_list": [
+      "example",
+      "disapora client"
+    ],
+    "owner": {
+      "id": 3,
+      "name": "Diaspora",
+      "created_at": "2013-09-30T13:46:02Z"
+    },
+    "name": "Diaspora Client",
+    "name_with_namespace": "Diaspora / Diaspora Client",
+    "path": "diaspora-client",
+    "path_with_namespace": "diaspora/diaspora-client",
+    "issues_enabled": true,
+    "open_issues_count": 1,
+    "merge_requests_enabled": true,
+    "builds_enabled": true,
+    "wiki_enabled": true,
+    "snippets_enabled": false,
+    "container_registry_enabled": false,
+    "created_at": "2013-09-30T13:46:02Z",
+    "last_activity_at": "2013-09-30T13:46:02Z",
+    "creator_id": 3,
+    "namespace": {
+      "created_at": "2013-09-30T13:46:02Z",
+      "description": "",
+      "id": 3,
+      "name": "Diaspora",
+      "owner_id": 1,
+      "path": "diaspora",
+      "updated_at": "2013-09-30T13:46:02Z"
+    },
+    "archived": false,
+    "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+    "shared_runners_enabled": true,
+    "forks_count": 0,
+    "star_count": 0,
+    "runners_token": "b8547b1dc37721d05889db52fa2f02",
+    "public_builds": true,
+    "shared_with_groups": [],
+    "only_allow_merge_if_build_succeeds": false,
+    "only_allow_merge_if_all_discussions_are_resolved": false,
+    "request_access_enabled": false
+  },
+  {
+    "id": 6,
+    "description": null,
+    "default_branch": "master",
+    "public": false,
+    "visibility_level": 0,
+    "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",
+    "tag_list": [
+      "example",
+      "puppet"
+    ],
+    "owner": {
+      "id": 4,
+      "name": "Brightbox",
+      "created_at": "2013-09-30T13:46:02Z"
+    },
+    "name": "Puppet",
+    "name_with_namespace": "Brightbox / Puppet",
+    "path": "puppet",
+    "path_with_namespace": "brightbox/puppet",
+    "issues_enabled": true,
+    "open_issues_count": 1,
+    "merge_requests_enabled": true,
+    "builds_enabled": true,
+    "wiki_enabled": true,
+    "snippets_enabled": false,
+    "container_registry_enabled": false,
+    "created_at": "2013-09-30T13:46:02Z",
+    "last_activity_at": "2013-09-30T13:46:02Z",
+    "creator_id": 3,
+    "namespace": {
+      "created_at": "2013-09-30T13:46:02Z",
+      "description": "",
+      "id": 4,
+      "name": "Brightbox",
+      "owner_id": 1,
+      "path": "brightbox",
+      "updated_at": "2013-09-30T13:46:02Z"
+    },
+    "permissions": {
+      "project_access": {
+        "access_level": 10,
+        "notification_level": 3
+      },
+      "group_access": {
+        "access_level": 50,
+        "notification_level": 3
+      }
+    },
+    "archived": false,
+    "avatar_url": null,
+    "shared_runners_enabled": true,
+    "forks_count": 0,
+    "star_count": 0,
+    "runners_token": "b8547b1dc37721d05889db52fa2f02",
+    "public_builds": true,
+    "shared_with_groups": [],
+    "only_allow_merge_if_build_succeeds": false,
+    "only_allow_merge_if_all_discussions_are_resolved": false,
+    "request_access_enabled": false
+  }
+]
+```
+
+Get a list of projects which the authenticated user can see.
+
+```
+GET /projects/visible
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `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 |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
 
 ```json
 [
@@ -159,11 +300,13 @@ GET /projects/owned
 
 Parameters:
 
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `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 |
 
 ### List starred projects
 
@@ -175,11 +318,13 @@ GET /projects/starred
 
 Parameters:
 
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `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 |
 
 ### List ALL projects
 
@@ -191,11 +336,13 @@ GET /projects/all
 
 Parameters:
 
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `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 |
 
 ### Get single project
 
@@ -208,7 +355,9 @@ GET /projects/:id
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
 
 ```json
 {
@@ -280,14 +429,17 @@ Parameters:
       "group_name": "Gitlab Org",
       "group_access_level": 10
     }
-  ]
+  ],
+  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_all_discussions_are_resolved": false,
+  "request_access_enabled": false
 }
 ```
 
 ### Get project events
 
 Get the events for the specified project.
-Sorted from newest to latest
+Sorted from newest to oldest
 
 ```
 GET /projects/:id/events
@@ -295,7 +447,9 @@ GET /projects/:id/events
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
 
 ```json
 [
@@ -314,7 +468,7 @@ Parameters:
       "id": 1,
       "state": "active",
       "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
-      "web_url": "http://localhost:3000/u/root"
+      "web_url": "http://localhost:3000/root"
     },
     "author_username": "root"
   },
@@ -331,7 +485,7 @@ Parameters:
       "id": 1,
       "state": "active",
       "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
-      "web_url": "http://localhost:3000/u/root"
+      "web_url": "http://localhost:3000/root"
     },
     "author_username": "john",
     "data": {
@@ -377,7 +531,7 @@ Parameters:
       "id": 1,
       "state": "active",
       "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
-      "web_url": "http://localhost:3000/u/root"
+      "web_url": "http://localhost:3000/root"
     },
     "author_username": "root"
   },
@@ -401,7 +555,7 @@ Parameters:
         "id": 1,
         "state": "active",
         "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
-        "web_url": "http://localhost:3000/u/root"
+        "web_url": "http://localhost:3000/root"
       },
       "created_at": "2015-12-04T10:33:56.698Z",
       "system": false,
@@ -416,7 +570,7 @@ Parameters:
       "id": 1,
       "state": "active",
       "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
-      "web_url": "http://localhost:3000/u/root"
+      "web_url": "http://localhost:3000/root"
     },
     "author_username": "root"
   }
@@ -433,21 +587,27 @@ POST /projects
 
 Parameters:
 
-- `name` (required) - new project name
-- `path` (optional) - custom repository name for new project. By default generated based on name
-- `namespace_id` (optional) - namespace for the new project (defaults to user)
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
+| 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 |
+| `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 |
+| `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 |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | 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 |
+| `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 |
 
 ### Create project for user
 
@@ -459,20 +619,28 @@ POST /projects/user/:user_id
 
 Parameters:
 
-- `user_id` (required) - user_id of owner
-- `name` (required) - new project name
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The user ID of the project owner |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `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 |
+| `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 |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | 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 |
+| `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 |
 
 ### Edit project
 
@@ -484,28 +652,34 @@ PUT /projects/:id
 
 Parameters:
 
-- `id` (required) - The ID of a project
-- `name` (optional) - project name
-- `path` (optional) - repository name for project
-- `description` (optional) - short project description
-- `default_branch` (optional)
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `public_builds` (optional)
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `name` | string | yes | The name of the project |
+| `path` | string | no | Custom repository name for the project. By default generated based on name |
+| `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 |
+| `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 |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | 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 |
+| `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 |
 
 On success, method returns 200 with the updated project. If parameters are
 invalid, 400 is returned.
 
 ### Fork project
 
-Forks a project into the user namespace of the authenticated user.
+Forks a project into the user namespace of the authenticated user or the one provided.
 
 ```
 POST /projects/fork/:id
@@ -513,7 +687,10 @@ POST /projects/fork/:id
 
 Parameters:
 
-- `id` (required) - The ID of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
 
 ### Star a project
 
@@ -524,9 +701,11 @@ Stars a given project. Returns status code `201` and the project on success and
 POST /projects/:id/star
 ```
 
+Parameters:
+
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes | The ID of the project |
+| `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"
@@ -577,7 +756,10 @@ Example response:
   "forks_count": 0,
   "star_count": 1,
   "public_builds": true,
-  "shared_with_groups": []
+  "shared_with_groups": [],
+  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_all_discussions_are_resolved": false,
+  "request_access_enabled": false
 }
 ```
 
@@ -592,7 +774,7 @@ DELETE /projects/:id/star
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes | The ID of the project |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ```bash
 curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -643,7 +825,10 @@ Example response:
   "forks_count": 0,
   "star_count": 0,
   "public_builds": true,
-  "shared_with_groups": []
+  "shared_with_groups": [],
+  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_all_discussions_are_resolved": false,
+  "request_access_enabled": false
 }
 ```
 
@@ -662,7 +847,7 @@ POST /projects/:id/archive
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes | The ID of the project |
+| `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/archive"
@@ -729,7 +914,10 @@ Example response:
   "star_count": 0,
   "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
   "public_builds": true,
-  "shared_with_groups": []
+  "shared_with_groups": [],
+  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_all_discussions_are_resolved": false,
+  "request_access_enabled": false
 }
 ```
 
@@ -748,7 +936,7 @@ POST /projects/:id/unarchive
 
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
-| `id`      | integer | yes | The ID of the project |
+| `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/unarchive"
@@ -815,7 +1003,10 @@ Example response:
   "star_count": 0,
   "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
   "public_builds": true,
-  "shared_with_groups": []
+  "shared_with_groups": [],
+  "only_allow_merge_if_build_succeeds": false,
+  "only_allow_merge_if_all_discussions_are_resolved": false,
+  "request_access_enabled": false
 }
 ```
 
@@ -829,7 +1020,9 @@ DELETE /projects/:id
 
 Parameters:
 
-- `id` (required) - The ID of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ## Uploads
 
@@ -843,8 +1036,10 @@ POST /projects/:id/uploads
 
 Parameters:
 
-- `id` (required) - The ID of the project
-- `file` (required) - The file to be uploaded
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `file` | string | yes | The file to be uploaded |
 
 ```json
 {
@@ -872,9 +1067,12 @@ POST /projects/:id/share
 
 Parameters:
 
-- `id` (required) - The ID of a project
-- `group_id` (required) - The ID of a group
-- `group_access` (required) - Level of permissions for sharing
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `group_id` | integer | yes | The ID of the group to share with |
+| `group_access` | integer | yes | The permissions level to grant the group |
+| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
 
 ## Hooks
 
@@ -891,7 +1089,9 @@ GET /projects/:id/hooks
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ### Get project hook
 
@@ -903,8 +1103,10 @@ GET /projects/:id/hooks/:hook_id
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of a project hook |
 
 ```json
 {
@@ -914,7 +1116,11 @@ Parameters:
   "push_events": true,
   "issues_events": true,
   "merge_requests_events": true,
+  "tag_push_events": true,
   "note_events": true,
+  "build_events": true,
+  "pipeline_events": true,
+  "wiki_page_events": true,
   "enable_ssl_verification": true,
   "created_at": "2012-10-12T17:04:47Z"
 }
@@ -930,14 +1136,20 @@ POST /projects/:id/hooks
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `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 |
+| `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 |
+| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
 
 ### Edit project hook
 
@@ -949,15 +1161,21 @@ PUT /projects/:id/hooks/:hook_id
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `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 |
+| `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 |
+| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
 
 ### Delete project hook
 
@@ -970,14 +1188,18 @@ DELETE /projects/:id/hooks/:hook_id
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of hook to delete
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
 
 Note the JSON response differs if the hook is available or not. If the project hook
 is available before it is returned in the JSON response or an empty response is returned.
 
 ## Branches
 
+For more information please consult the [Branches](branches.md) documentation.
+
 ### List branches
 
 Lists all branches of a project.
@@ -988,7 +1210,9 @@ GET /projects/:id/repository/branches
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ```json
 [
@@ -996,56 +1220,46 @@ Parameters:
     "name": "async",
     "commit": {
       "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
-      "parents": [
-        {
-          "id": "3f94fc7c85061973edc9906ae170cc269b07ca55"
-        }
+      "parent_ids": [
+        "3f94fc7c85061973edc9906ae170cc269b07ca55"
       ],
-      "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee",
       "message": "give Caolan credit where it's due (up top)",
-      "author": {
-        "name": "Jeremy Ashkenas",
-        "email": "jashkenas@example.com"
-      },
-      "committer": {
-        "name": "Jeremy Ashkenas",
-        "email": "jashkenas@example.com"
-      },
+      "author_name": "Jeremy Ashkenas",
+      "author_email": "jashkenas@example.com",
       "authored_date": "2010-12-08T21:28:50+00:00",
+      "committer_name": "Jeremy Ashkenas",
+      "committer_email": "jashkenas@example.com",
       "committed_date": "2010-12-08T21:28:50+00:00"
     },
-    "protected": false
+    "protected": false,
+    "developers_can_push": false,
+    "developers_can_merge": false
   },
   {
     "name": "gh-pages",
     "commit": {
       "id": "101c10a60019fe870d21868835f65c25d64968fc",
-      "parents": [
-        {
-          "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
-        }
+      "parent_ids": [
+          "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
       ],
-      "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a",
       "message": "Underscore.js 1.5.2",
-      "author": {
-        "name": "Jeremy Ashkenas",
-        "email": "jashkenas@example.com"
-      },
-      "committer": {
-        "name": "Jeremy Ashkenas",
-        "email": "jashkenas@example.com"
-      },
+      "author_name": "Jeremy Ashkenas",
+      "author_email": "jashkenas@example.com",
       "authored_date": "2013-09-07T12:58:21+00:00",
+      "committer_name": "Jeremy Ashkenas",
+      "committer_email": "jashkenas@example.com",
       "committed_date": "2013-09-07T12:58:21+00:00"
     },
-    "protected": false
+    "protected": false,
+    "developers_can_push": false,
+    "developers_can_merge": false
   }
 ]
 ```
 
-### List single branch
+### Single branch
 
-Lists a specific branch of a project.
+A specific branch of a project.
 
 ```
 GET /projects/:id/repository/branches/:branch
@@ -1053,8 +1267,12 @@ GET /projects/:id/repository/branches/:branch
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
+| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
+| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
 
 ### Protect single branch
 
@@ -1066,8 +1284,10 @@ PUT /projects/:id/repository/branches/:branch/protect
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
 
 ### Unprotect single branch
 
@@ -1079,8 +1299,10 @@ PUT /projects/:id/repository/branches/:branch/unprotect
 
 Parameters:
 
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
 
 ## Admin fork relation
 
@@ -1094,8 +1316,10 @@ POST /projects/:id/fork/:forked_from_id
 
 Parameters:
 
-- `id` (required) - The ID of the project
-- `forked_from_id:` (required) - The ID of the project that was forked from
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `forked_from_id` | ID | yes | The ID of the project that was forked from |
 
 ### Delete an existing forked from relationship
 
@@ -1105,7 +1329,9 @@ DELETE /projects/:id/fork
 
 Parameter:
 
-- `id` (required) - The ID of the project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
 
 ## Search for projects by name
 
@@ -1117,8 +1343,8 @@ GET /projects/search/:query
 
 Parameters:
 
-- `query` (required) - A string contained in the project name
-- `per_page` (optional) - number of projects to return per page
-- `page` (optional) - the page to retrieve
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `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 |
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index fc3af5544de021765f451124ae26faf8593f309d..1bc6a24e914656115fcfb3e57974d79627ef7dd1 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -44,7 +44,7 @@ POST /projects/:id/repository/files
 ```
 
 ```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
 ```
 
 Example response:
@@ -61,6 +61,8 @@ Parameters:
 - `file_path` (required) - Full path to new file. Ex. lib/class.rb
 - `branch_name` (required) - The name of branch
 - `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
 - `content` (required) - File content
 - `commit_message` (required) - Commit message
 
@@ -71,7 +73,7 @@ PUT /projects/:id/repository/files
 ```
 
 ```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
 ```
 
 Example response:
@@ -88,6 +90,8 @@ Parameters:
 - `file_path` (required) - Full path to file. Ex. lib/class.rb
 - `branch_name` (required) - The name of branch
 - `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
 - `content` (required) - New file content
 - `commit_message` (required) - Commit message
 
@@ -107,7 +111,7 @@ DELETE /projects/:id/repository/files
 ```
 
 ```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
 ```
 
 Example response:
@@ -123,4 +127,6 @@ Parameters:
 
 - `file_path` (required) - Full path to file. Ex. lib/class.rb
 - `branch_name` (required) - The name of branch
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
 - `commit_message` (required) - Commit message
diff --git a/doc/api/services.md b/doc/api/services.md
index 579fdc0c8c971066bf91bc3f7eef7003aac45755..c7f537aceb652a3fc72b687f4f1dde00049ad30b 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -451,43 +451,49 @@ GET /projects/:id/services/irker
 
 ## JIRA
 
-Jira issue tracker
+JIRA issue tracker.
+
+### Get JIRA service settings
+
+Get JIRA service settings for a project.
+
+```
+GET /projects/:id/services/jira
+```
 
 ### Create/Edit JIRA service
 
 Set JIRA service for a project.
 
-> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details.  Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html)
+>**Note:**
+Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to
+easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc]
+for details.
 
 ```
 PUT /projects/:id/services/jira
 ```
 
-Parameters:
-
-- `new_issue_url` (**required**) - New Issue url
-- `project_url` (**required**) - Project url
-- `issues_url` (**required**) - Issue url
-- `description` (optional) - Jira issue tracker
-- `username` (optional) - Jira username
-- `password` (optional) - Jira password
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `active`        | boolean| no  | Enable/disable the JIRA service. |
+| `project_url`   | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
+| `issues_url`    | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.|
+| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
+| `api_url`       | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
+| `description`   | string | no  | A name for the issue tracker. |
+| `username`      | string | no  | The username of the user created to be used with GitLab/JIRA. |
+| `password`      | string | no  | The password of the user created to be used with GitLab/JIRA. |
+| `jira_issue_transition_id` | string | no | 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`. |
 
 ### Delete JIRA service
 
-Delete JIRA service for a project.
+Remove all previously JIRA settings from a project.
 
 ```
 DELETE /projects/:id/services/jira
 ```
 
-### Get JIRA service settings
-
-Get JIRA service settings for a project.
-
-```
-GET /projects/:id/services/jira
-```
-
 ## PivotalTracker
 
 Project Management Software (Source Commits Endpoint)
@@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project.
 ```
 GET /projects/:id/services/teamcity
 ```
+
+[jira-doc]: ../project_services/jira.md
diff --git a/doc/api/session.md b/doc/api/session.md
index 9076c48b899decf0a1e1fa54ceeb30c677f1d668..f776424023e8eacebc9fcfd9c36914ececdcc447 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -2,7 +2,7 @@
 
 ## Deprecation Notice
 
-1. Starting in GitLab 9.0, this feature will be *disabled* for users with two-factor authentication turned on.
+1. Starting in GitLab 8.11, this feature has been *disabled* for users with two-factor authentication turned on.
 2. These users can access the API using [personal access tokens] instead.
 
 ---
diff --git a/doc/api/settings.md b/doc/api/settings.md
index a76dad0ebd47b9d2d1b1f3d15167d3c1f9321f96..218546aafea0e296e0bd2d980aa6ef49dffd60d5 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -41,7 +41,10 @@ Example response:
    "gravatar_enabled" : true,
    "sign_in_text" : null,
    "container_registry_token_expire_delay": 5,
-   "repository_storage": "default"
+   "repository_storage": "default",
+   "repository_storages": ["default"],
+   "koding_enabled": false,
+   "koding_url": null
 }
 ```
 
@@ -67,12 +70,15 @@ PUT /application/settings
 | `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`.|
 | `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_whitelist_enabled` is `true` | People trying to sign-up with emails from this domain will not be allowed to do so. |
+| `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. |
 | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
 | `after_sign_out_path` | string | no | Where to redirect users after logout |
 | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
-| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
-| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
+| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
+| `repository_storage` | string | no | The first entry in `repository_storages`. Deprecated, but retained for compatibility reasons |
+| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
+| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
+| `koding_url` | string | yes (if `koding_enabled` is `true`) |  The Koding instance URL for integration. |
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -103,6 +109,8 @@ Example response:
   "user_oauth_applications": true,
   "after_sign_out_path": "",
   "container_registry_token_expire_delay": 5,
-  "repository_storage": "default"
+  "repository_storage": "default",
+  "koding_enabled": false,
+  "koding_url": null
 }
 ```
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 1802fae14feb8c87782b4eaac7a2c5c6607c3d99..efd23d514bc637350b2b2d52076427e30345e427 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -27,11 +27,14 @@ Example response:
 
 ```json
 [
-   {
-      "id" : 1,
-      "url" : "https://gitlab.example.com/hook",
-      "created_at" : "2015-11-04T20:07:35.874Z"
-   }
+  {
+    "id":1,
+    "url":"https://gitlab.example.com/hook",
+    "created_at":"2016-10-31T12:32:15.192Z",
+    "push_events":true,
+    "tag_push_events":false,
+    "enable_ssl_verification":true
+  }
 ]
 ```
 
@@ -48,6 +51,10 @@ POST /hooks
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `url` | string | yes | The hook URL |
+| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
+| `push_events` | boolean |  no | When true, the hook will fire on push events |
+| `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
 
 Example request:
 
@@ -59,11 +66,14 @@ Example response:
 
 ```json
 [
-   {
-      "id" : 2,
-      "url" : "https://gitlab.example.com/hook",
-      "created_at" : "2015-11-04T20:07:35.874Z"
-   }
+  {
+    "id":1,
+    "url":"https://gitlab.example.com/hook",
+    "created_at":"2016-10-31T12:32:15.192Z",
+    "push_events":true,
+    "tag_push_events":false,
+    "enable_ssl_verification":true
+  }
 ]
 ```
 
@@ -98,11 +108,8 @@ Example response:
 
 ## Delete system hook
 
-Deletes a system hook. This is an idempotent API function and returns `200 OK`
-even if the hook is not available.
-
-If the hook is deleted, a JSON object is returned. An error is raised if the
-hook is not found.
+Deletes a system hook. It returns `200 OK` if the hooks is deleted and
+`404 Not Found` if the hook is not found.
 
 ---
 
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 5405911745653f6d0ebe3edfe8231158f87d3dad..398b080e3f672b6f7ffe4ede3806ad8d9aa693d7 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -124,7 +124,7 @@ Parameters:
 The message will be `nil` when creating a lightweight tag otherwise
 it will contain the annotation.
 
-It returns 200 if the operation succeed. In case of an error,
+It returns 201 if the operation succeed. In case of an error,
 405 with an explaining error message is returned.
 
 ## Delete a tag
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
new file mode 100644
index 0000000000000000000000000000000000000000..8235be92b12df98db7c77b1d9a8dd30649f5a291
--- /dev/null
+++ b/doc/api/templates/gitignores.md
@@ -0,0 +1,579 @@
+# Gitignores
+
+## List gitignore templates
+
+Get all gitignore templates.
+
+```
+GET /templates/gitignores
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores
+```
+
+Example response:
+
+```json
+[
+  {
+    "name": "AppEngine"
+  },
+  {
+    "name": "Laravel"
+  },
+  {
+    "name": "Elisp"
+  },
+  {
+    "name": "SketchUp"
+  },
+  {
+    "name": "Ada"
+  },
+  {
+    "name": "Ruby"
+  },
+  {
+    "name": "Kohana"
+  },
+  {
+    "name": "Nanoc"
+  },
+  {
+    "name": "Erlang"
+  },
+  {
+    "name": "OCaml"
+  },
+  {
+    "name": "Lithium"
+  },
+  {
+    "name": "Fortran"
+  },
+  {
+    "name": "Scala"
+  },
+  {
+    "name": "Node"
+  },
+  {
+    "name": "Fancy"
+  },
+  {
+    "name": "Perl"
+  },
+  {
+    "name": "Zephir"
+  },
+  {
+    "name": "WordPress"
+  },
+  {
+    "name": "Symfony"
+  },
+  {
+    "name": "FuelPHP"
+  },
+  {
+    "name": "DM"
+  },
+  {
+    "name": "Sdcc"
+  },
+  {
+    "name": "Rust"
+  },
+  {
+    "name": "C"
+  },
+  {
+    "name": "Umbraco"
+  },
+  {
+    "name": "Actionscript"
+  },
+  {
+    "name": "Android"
+  },
+  {
+    "name": "Grails"
+  },
+  {
+    "name": "Composer"
+  },
+  {
+    "name": "ExpressionEngine"
+  },
+  {
+    "name": "Gcov"
+  },
+  {
+    "name": "Qt"
+  },
+  {
+    "name": "Phalcon"
+  },
+  {
+    "name": "ArchLinuxPackages"
+  },
+  {
+    "name": "TeX"
+  },
+  {
+    "name": "SCons"
+  },
+  {
+    "name": "Lilypond"
+  },
+  {
+    "name": "CommonLisp"
+  },
+  {
+    "name": "Rails"
+  },
+  {
+    "name": "Mercury"
+  },
+  {
+    "name": "Magento"
+  },
+  {
+    "name": "ChefCookbook"
+  },
+  {
+    "name": "GitBook"
+  },
+  {
+    "name": "C++"
+  },
+  {
+    "name": "Eagle"
+  },
+  {
+    "name": "Go"
+  },
+  {
+    "name": "OpenCart"
+  },
+  {
+    "name": "Scheme"
+  },
+  {
+    "name": "Typo3"
+  },
+  {
+    "name": "SeamGen"
+  },
+  {
+    "name": "Swift"
+  },
+  {
+    "name": "Elm"
+  },
+  {
+    "name": "Unity"
+  },
+  {
+    "name": "Agda"
+  },
+  {
+    "name": "CUDA"
+  },
+  {
+    "name": "VVVV"
+  },
+  {
+    "name": "Finale"
+  },
+  {
+    "name": "LemonStand"
+  },
+  {
+    "name": "Textpattern"
+  },
+  {
+    "name": "Julia"
+  },
+  {
+    "name": "Packer"
+  },
+  {
+    "name": "Scrivener"
+  },
+  {
+    "name": "Dart"
+  },
+  {
+    "name": "Plone"
+  },
+  {
+    "name": "Jekyll"
+  },
+  {
+    "name": "Xojo"
+  },
+  {
+    "name": "LabVIEW"
+  },
+  {
+    "name": "Autotools"
+  },
+  {
+    "name": "KiCad"
+  },
+  {
+    "name": "Prestashop"
+  },
+  {
+    "name": "ROS"
+  },
+  {
+    "name": "Smalltalk"
+  },
+  {
+    "name": "GWT"
+  },
+  {
+    "name": "OracleForms"
+  },
+  {
+    "name": "SugarCRM"
+  },
+  {
+    "name": "Nim"
+  },
+  {
+    "name": "SymphonyCMS"
+  },
+  {
+    "name": "Maven"
+  },
+  {
+    "name": "CFWheels"
+  },
+  {
+    "name": "Python"
+  },
+  {
+    "name": "ZendFramework"
+  },
+  {
+    "name": "CakePHP"
+  },
+  {
+    "name": "Concrete5"
+  },
+  {
+    "name": "PlayFramework"
+  },
+  {
+    "name": "Terraform"
+  },
+  {
+    "name": "Elixir"
+  },
+  {
+    "name": "CMake"
+  },
+  {
+    "name": "Joomla"
+  },
+  {
+    "name": "Coq"
+  },
+  {
+    "name": "Delphi"
+  },
+  {
+    "name": "Haskell"
+  },
+  {
+    "name": "Yii"
+  },
+  {
+    "name": "Java"
+  },
+  {
+    "name": "UnrealEngine"
+  },
+  {
+    "name": "AppceleratorTitanium"
+  },
+  {
+    "name": "CraftCMS"
+  },
+  {
+    "name": "ForceDotCom"
+  },
+  {
+    "name": "ExtJs"
+  },
+  {
+    "name": "MetaProgrammingSystem"
+  },
+  {
+    "name": "D"
+  },
+  {
+    "name": "Objective-C"
+  },
+  {
+    "name": "RhodesRhomobile"
+  },
+  {
+    "name": "R"
+  },
+  {
+    "name": "EPiServer"
+  },
+  {
+    "name": "Yeoman"
+  },
+  {
+    "name": "VisualStudio"
+  },
+  {
+    "name": "Processing"
+  },
+  {
+    "name": "Leiningen"
+  },
+  {
+    "name": "Stella"
+  },
+  {
+    "name": "Opa"
+  },
+  {
+    "name": "Drupal"
+  },
+  {
+    "name": "TurboGears2"
+  },
+  {
+    "name": "Idris"
+  },
+  {
+    "name": "Jboss"
+  },
+  {
+    "name": "CodeIgniter"
+  },
+  {
+    "name": "Qooxdoo"
+  },
+  {
+    "name": "Waf"
+  },
+  {
+    "name": "Sass"
+  },
+  {
+    "name": "Lua"
+  },
+  {
+    "name": "Clojure"
+  },
+  {
+    "name": "IGORPro"
+  },
+  {
+    "name": "Gradle"
+  },
+  {
+    "name": "Archives"
+  },
+  {
+    "name": "SynopsysVCS"
+  },
+  {
+    "name": "Ninja"
+  },
+  {
+    "name": "Tags"
+  },
+  {
+    "name": "OSX"
+  },
+  {
+    "name": "Dreamweaver"
+  },
+  {
+    "name": "CodeKit"
+  },
+  {
+    "name": "NotepadPP"
+  },
+  {
+    "name": "VisualStudioCode"
+  },
+  {
+    "name": "Mercurial"
+  },
+  {
+    "name": "BricxCC"
+  },
+  {
+    "name": "DartEditor"
+  },
+  {
+    "name": "Eclipse"
+  },
+  {
+    "name": "Cloud9"
+  },
+  {
+    "name": "TortoiseGit"
+  },
+  {
+    "name": "NetBeans"
+  },
+  {
+    "name": "GPG"
+  },
+  {
+    "name": "Espresso"
+  },
+  {
+    "name": "Redcar"
+  },
+  {
+    "name": "Xcode"
+  },
+  {
+    "name": "Matlab"
+  },
+  {
+    "name": "LyX"
+  },
+  {
+    "name": "SlickEdit"
+  },
+  {
+    "name": "Dropbox"
+  },
+  {
+    "name": "CVS"
+  },
+  {
+    "name": "Calabash"
+  },
+  {
+    "name": "JDeveloper"
+  },
+  {
+    "name": "Vagrant"
+  },
+  {
+    "name": "IPythonNotebook"
+  },
+  {
+    "name": "TextMate"
+  },
+  {
+    "name": "Ensime"
+  },
+  {
+    "name": "WebMethods"
+  },
+  {
+    "name": "VirtualEnv"
+  },
+  {
+    "name": "Emacs"
+  },
+  {
+    "name": "Momentics"
+  },
+  {
+    "name": "JetBrains"
+  },
+  {
+    "name": "SublimeText"
+  },
+  {
+    "name": "Kate"
+  },
+  {
+    "name": "ModelSim"
+  },
+  {
+    "name": "Redis"
+  },
+  {
+    "name": "KDevelop4"
+  },
+  {
+    "name": "Bazaar"
+  },
+  {
+    "name": "Linux"
+  },
+  {
+    "name": "Windows"
+  },
+  {
+    "name": "XilinxISE"
+  },
+  {
+    "name": "Lazarus"
+  },
+  {
+    "name": "EiffelStudio"
+  },
+  {
+    "name": "Anjuta"
+  },
+  {
+    "name": "Vim"
+  },
+  {
+    "name": "Otto"
+  },
+  {
+    "name": "MicrosoftOffice"
+  },
+  {
+    "name": "LibreOffice"
+  },
+  {
+    "name": "SBT"
+  },
+  {
+    "name": "MonoDevelop"
+  },
+  {
+    "name": "SVN"
+  },
+  {
+    "name": "FlexBuilder"
+  }
+]
+```
+
+## Single gitignore template
+
+Get a single gitignore template.
+
+```
+GET /templates/gitignores/:key
+```
+
+| Attribute  | Type   | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key`      | string | yes      | The key of the gitignore template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby
+```
+
+Example response:
+
+```json
+{
+  "name": "Ruby",
+  "content": "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/\n/spec/examples.txt\n/test/tmp/\n/test/version_tmp/\n/tmp/\n\n# Used by dotenv library to load environment variables.\n# .env\n\n## Specific to RubyMotion:\n.dat*\n.repl_history\nbuild/\n*.bridgesupport\nbuild-iPhoneOS/\nbuild-iPhoneSimulator/\n\n## Specific to RubyMotion (use of CocoaPods):\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n# vendor/Pods/\n\n## Documentation cache and generated files:\n/.yardoc/\n/_yardoc/\n/doc/\n/rdoc/\n\n## Environment normalization:\n/.bundle/\n/vendor/bundle\n/lib/bundler/man/\n\n# for a library or gem, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# Gemfile.lock\n# .ruby-version\n# .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n"
+}
+```
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
new file mode 100644
index 0000000000000000000000000000000000000000..e120016fbe6b1b17fe66bbf4f29c52e50132f2d7
--- /dev/null
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -0,0 +1,120 @@
+# GitLab CI YMLs
+
+## List GitLab CI YML templates
+
+Get all GitLab CI YML templates.
+
+```
+GET /templates/gitlab_ci_ymls
+```
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls
+```
+
+Example response:
+
+```json
+[
+  {
+    "name": "C++"
+  },
+  {
+    "name": "Docker"
+  },
+  {
+    "name": "Elixir"
+  },
+  {
+    "name": "LaTeX"
+  },
+  {
+    "name": "Grails"
+  },
+  {
+    "name": "Rust"
+  },
+  {
+    "name": "Nodejs"
+  },
+  {
+    "name": "Ruby"
+  },
+  {
+    "name": "Scala"
+  },
+  {
+    "name": "Maven"
+  },
+  {
+    "name": "Harp"
+  },
+  {
+    "name": "Pelican"
+  },
+  {
+    "name": "Hyde"
+  },
+  {
+    "name": "Nanoc"
+  },
+  {
+    "name": "Octopress"
+  },
+  {
+    "name": "JBake"
+  },
+  {
+    "name": "HTML"
+  },
+  {
+    "name": "Hugo"
+  },
+  {
+    "name": "Metalsmith"
+  },
+  {
+    "name": "Hexo"
+  },
+  {
+    "name": "Lektor"
+  },
+  {
+    "name": "Doxygen"
+  },
+  {
+    "name": "Brunch"
+  },
+  {
+    "name": "Jekyll"
+  },
+  {
+    "name": "Middleman"
+  }
+]
+```
+
+## Single GitLab CI YML template
+
+Get a single GitLab CI YML template.
+
+```
+GET /templates/gitlab_ci_ymls/:key
+```
+
+| Attribute  | Type   | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key`      | string | yes      | The key of the GitLab CI YML template |
+
+```bash
+curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby
+```
+
+Example response:
+
+```json
+{
+  "name": "Ruby",
+  "content": "# This file is a template, and might need editing before it works on your project.\n# Official language image. Look for the different tagged releases at:\n# https://hub.docker.com/r/library/ruby/tags/\nimage: \"ruby:2.3\"\n\n# Pick zero or more services to be used on all builds.\n# Only needed when using a docker container to run your tests in.\n# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service\nservices:\n  - mysql:latest\n  - redis:latest\n  - postgres:latest\n\nvariables:\n  POSTGRES_DB: database_name\n\n# Cache gems in between builds\ncache:\n  paths:\n    - vendor/ruby\n\n# This is a basic example for a gem or script which doesn't use\n# services such as redis or postgres\nbefore_script:\n  - ruby -v                                   # Print out ruby version for debugging\n  # Uncomment next line if your rails app needs a JS runtime:\n  # - apt-get update -q && apt-get install nodejs -yqq\n  - gem install bundler  --no-ri --no-rdoc    # Bundler is not installed with the image\n  - bundle install -j $(nproc) --path vendor  # Install dependencies into ./vendor/ruby\n\n# Optional - Delete if not using `rubocop`\nrubocop:\n  script:\n  - rubocop\n\nrspec:\n  script:\n  - rspec spec\n\nrails:\n  variables:\n    DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB\"\n  script:\n  - bundle exec rake db:migrate\n  - bundle exec rake db:seed\n  - bundle exec rake test\n"
+}
+```
diff --git a/doc/api/licenses.md b/doc/api/templates/licenses.md
similarity index 95%
rename from doc/api/licenses.md
rename to doc/api/templates/licenses.md
index ed26d1fb7fbf777c843948148a57abc6fa732d37..ae7218cf1bdf5b906b5c0e8cb78ed8e3b97dc906 100644
--- a/doc/api/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -5,7 +5,7 @@
 Get all license templates.
 
 ```
-GET /licenses
+GET /templates/licenses
 ```
 
 | Attribute | Type    | Required | Description           |
@@ -13,7 +13,7 @@ GET /licenses
 | `popular` | boolean | no       | If passed, returns only popular licenses |
 
 ```bash
-curl https://gitlab.example.com/api/v3/licenses?popular=1
+curl https://gitlab.example.com/api/v3/templates/licenses?popular=1
 ```
 
 Example response:
@@ -102,7 +102,7 @@ Get a single license template. You can pass parameters to replace the license
 placeholder.
 
 ```
-GET /licenses/:key
+GET /templates/licenses/:key
 ```
 
 | Attribute  | Type   | Required | Description |
@@ -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/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project
 ```
 
 Example response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 0cd644dfd2fe2bbd4e9e84cab7b990fa2e1fecda..a5e818010247a750a9542f1cf5051477c741611d 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -44,7 +44,7 @@ Example Response:
       "id": 1,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/root"
+      "web_url": "https://gitlab.example.com/root"
     },
     "action_name": "marked",
     "target_type": "MergeRequest",
@@ -67,7 +67,7 @@ Example Response:
         "id": 12,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/craig_rutherford"
+        "web_url": "https://gitlab.example.com/craig_rutherford"
       },
       "assignee": {
         "name": "Administrator",
@@ -75,7 +75,7 @@ Example Response:
         "id": 1,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/root"
+        "web_url": "https://gitlab.example.com/root"
       },
       "source_project_id": 2,
       "target_project_id": 2,
@@ -117,7 +117,7 @@ Example Response:
       "id": 12,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/craig_rutherford"
+      "web_url": "https://gitlab.example.com/craig_rutherford"
     },
     "action_name": "assigned",
     "target_type": "MergeRequest",
@@ -140,7 +140,7 @@ Example Response:
         "id": 12,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/craig_rutherford"
+        "web_url": "https://gitlab.example.com/craig_rutherford"
       },
       "assignee": {
         "name": "Administrator",
@@ -148,7 +148,7 @@ Example Response:
         "id": 1,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/root"
+        "web_url": "https://gitlab.example.com/root"
       },
       "source_project_id": 2,
       "target_project_id": 2,
@@ -215,7 +215,7 @@ Example Response:
       "id": 1,
       "state": "active",
       "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "https://gitlab.example.com/u/root"
+      "web_url": "https://gitlab.example.com/root"
     },
     "action_name": "marked",
     "target_type": "MergeRequest",
@@ -238,7 +238,7 @@ Example Response:
         "id": 12,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/craig_rutherford"
+        "web_url": "https://gitlab.example.com/craig_rutherford"
       },
       "assignee": {
         "name": "Administrator",
@@ -246,7 +246,7 @@ Example Response:
         "id": 1,
         "state": "active",
         "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-        "web_url": "https://gitlab.example.com/u/root"
+        "web_url": "https://gitlab.example.com/root"
       },
       "source_project_id": 2,
       "target_project_id": 2,
diff --git a/doc/api/users.md b/doc/api/users.md
index 7e848586dbd02e11c2b3da7b716e4c249ebc9954..041df07c0515048cfa18f84a1fd74f9767cdb2f5 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -20,7 +20,7 @@ GET /users
     "name": "John Smith",
     "state": "active",
     "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
-    "web_url": "http://localhost:3000/u/john_smith"
+    "web_url": "http://localhost:3000/john_smith"
   },
   {
     "id": 2,
@@ -28,11 +28,23 @@ GET /users
     "name": "Jack Smith",
     "state": "blocked",
     "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
-    "web_url": "http://localhost:3000/u/jack_smith"
+    "web_url": "http://localhost:3000/jack_smith"
   }
 ]
 ```
 
+In addition, you can filter users based on states eg. `blocked`, `active`
+This works only to filter users who are `blocked` or `active`.
+It does not support `active=false` or `blocked=false`.
+
+```
+GET /users?active=true
+```
+
+```
+GET /users?blocked=true
+```
+
 ### For admins
 
 ```
@@ -48,7 +60,7 @@ GET /users
     "name": "John Smith",
     "state": "active",
     "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
-    "web_url": "http://localhost:3000/u/john_smith",
+    "web_url": "http://localhost:3000/john_smith",
     "created_at": "2012-05-23T08:00:58Z",
     "is_admin": false,
     "bio": null,
@@ -57,6 +69,7 @@ GET /users
     "linkedin": "",
     "twitter": "",
     "website_url": "",
+    "organization": "",
     "last_sign_in_at": "2012-06-01T11:41:01Z",
     "confirmed_at": "2012-05-23T09:05:22Z",
     "theme_id": 1,
@@ -80,7 +93,7 @@ GET /users
     "name": "Jack Smith",
     "state": "blocked",
     "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
-    "web_url": "http://localhost:3000/u/jack_smith",
+    "web_url": "http://localhost:3000/jack_smith",
     "created_at": "2012-05-23T08:01:01Z",
     "is_admin": false,
     "bio": null,
@@ -89,6 +102,7 @@ GET /users
     "linkedin": "",
     "twitter": "",
     "website_url": "",
+    "organization": "",
     "last_sign_in_at": null,
     "confirmed_at": "2012-05-30T16:53:06.148Z",
     "theme_id": 1,
@@ -118,6 +132,8 @@ For example:
 GET /users?username=jack_smith
 ```
 
+You can search for users who are external with: `/users?external=true`
+
 ## Single user
 
 Get a single user.
@@ -139,7 +155,7 @@ Parameters:
   "name": "John Smith",
   "state": "active",
   "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
-  "web_url": "http://localhost:3000/u/john_smith",
+  "web_url": "http://localhost:3000/john_smith",
   "created_at": "2012-05-23T08:00:58Z",
   "is_admin": false,
   "bio": null,
@@ -147,7 +163,8 @@ Parameters:
   "skype": "",
   "linkedin": "",
   "twitter": "",
-  "website_url": ""
+  "website_url": "",
+  "organization": ""
 }
 ```
 
@@ -169,7 +186,7 @@ Parameters:
   "name": "John Smith",
   "state": "active",
   "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
-  "web_url": "http://localhost:3000/u/john_smith",
+  "web_url": "http://localhost:3000/john_smith",
   "created_at": "2012-05-23T08:00:58Z",
   "is_admin": false,
   "bio": null,
@@ -178,6 +195,7 @@ Parameters:
   "linkedin": "",
   "twitter": "",
   "website_url": "",
+  "organization": "",
   "last_sign_in_at": "2012-06-01T11:41:01Z",
   "confirmed_at": "2012-05-23T09:05:22Z",
   "theme_id": 1,
@@ -214,6 +232,7 @@ Parameters:
 - `linkedin` (optional)         - LinkedIn
 - `twitter` (optional)          - Twitter account
 - `website_url` (optional)      - Website URL
+- `organization` (optional)     - Organization name
 - `projects_limit` (optional)   - Number of projects user can create
 - `extern_uid` (optional)       - External UID
 - `provider` (optional)         - External provider name
@@ -242,6 +261,7 @@ Parameters:
 - `linkedin`                    - LinkedIn
 - `twitter`                     - Twitter account
 - `website_url`                 - Website URL
+- `organization`                - Organization name
 - `projects_limit`              - Limit projects each user can create
 - `extern_uid`                  - External UID
 - `provider`                    - External provider name
@@ -287,7 +307,7 @@ GET /user
   "name": "John Smith",
   "state": "active",
   "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
-  "web_url": "http://localhost:3000/u/john_smith",
+  "web_url": "http://localhost:3000/john_smith",
   "created_at": "2012-05-23T08:00:58Z",
   "is_admin": false,
   "bio": null,
@@ -296,6 +316,7 @@ GET /user
   "linkedin": "",
   "twitter": "",
   "website_url": "",
+  "organization": "",
   "last_sign_in_at": "2012-06-01T11:41:01Z",
   "confirmed_at": "2012-05-23T09:05:22Z",
   "theme_id": 1,
@@ -310,8 +331,7 @@ GET /user
   "can_create_group": true,
   "can_create_project": true,
   "two_factor_enabled": true,
-  "external": false,
-  "private_token": "dd34asd13as"
+  "external": false
 }
 ```
 
@@ -621,3 +641,149 @@ Parameters:
 
 Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
 `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
+
+### Get user contribution events
+
+Get the contribution events for the specified user, sorted from newest to oldest.
+
+```
+GET /users/:id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events
+```
+
+Example response:
+
+```json
+[
+  {
+    "title": null,
+    "project_id": 15,
+    "action_name": "closed",
+    "target_id": 830,
+    "target_type": "Issue",
+    "author_id": 1,
+    "data": null,
+    "target_title": "Public project search field",
+    "author": {
+      "name": "Dmitriy Zaporozhets",
+      "username": "root",
+      "id": 1,
+      "state": "active",
+      "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+      "web_url": "http://localhost:3000/root"
+    },
+    "author_username": "root"
+  },
+  {
+    "title": null,
+    "project_id": 15,
+    "action_name": "opened",
+    "target_id": null,
+    "target_type": null,
+    "author_id": 1,
+    "author": {
+      "name": "Dmitriy Zaporozhets",
+      "username": "root",
+      "id": 1,
+      "state": "active",
+      "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+      "web_url": "http://localhost:3000/root"
+    },
+    "author_username": "john",
+    "data": {
+      "before": "50d4420237a9de7be1304607147aec22e4a14af7",
+      "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+      "ref": "refs/heads/master",
+      "user_id": 1,
+      "user_name": "Dmitriy Zaporozhets",
+      "repository": {
+        "name": "gitlabhq",
+        "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
+        "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
+        "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
+      },
+      "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
+    },
+    "target_title": null
+  },
+  {
+    "title": null,
+    "project_id": 15,
+    "action_name": "closed",
+    "target_id": 840,
+    "target_type": "Issue",
+    "author_id": 1,
+    "data": null,
+    "target_title": "Finish & merge Code search PR",
+    "author": {
+      "name": "Dmitriy Zaporozhets",
+      "username": "root",
+      "id": 1,
+      "state": "active",
+      "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+      "web_url": "http://localhost:3000/root"
+    },
+    "author_username": "root"
+  },
+  {
+    "title": null,
+    "project_id": 15,
+    "action_name": "commented on",
+    "target_id": 1312,
+    "target_type": "Note",
+    "author_id": 1,
+    "data": null,
+    "target_title": null,
+    "created_at": "2015-12-04T10:33:58.089Z",
+    "note": {
+      "id": 1312,
+      "body": "What an awesome day!",
+      "attachment": null,
+      "author": {
+        "name": "Dmitriy Zaporozhets",
+        "username": "root",
+        "id": 1,
+        "state": "active",
+        "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+        "web_url": "http://localhost:3000/root"
+      },
+      "created_at": "2015-12-04T10:33:56.698Z",
+      "system": false,
+      "upvote": false,
+      "downvote": false,
+      "noteable_id": 377,
+      "noteable_type": "Issue"
+    },
+    "author": {
+      "name": "Dmitriy Zaporozhets",
+      "username": "root",
+      "id": 1,
+      "state": "active",
+      "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+      "web_url": "http://localhost:3000/root"
+    },
+    "author_username": "root"
+  }
+]
+```
diff --git a/doc/api/version.md b/doc/api/version.md
new file mode 100644
index 0000000000000000000000000000000000000000..287d17cf97f841bfdec210a1f1382867a72c040f
--- /dev/null
+++ b/doc/api/version.md
@@ -0,0 +1,23 @@
+# Version API
+
+>**Note:** This feature was introduced in GitLab 8.13
+
+Retrieve version information for this GitLab instance. Responds `200 OK` for
+authenticated users.
+
+```
+GET /version
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version
+```
+
+Example response:
+
+```json
+{
+  "version": "8.13.0-pre",
+  "revision": "4e963fe"
+}
+```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 10ce4ac8940024e6f0da941de49e5e7118152ac3..6b90940c047acc12498e256f71a34509a3942c6b 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -16,5 +16,8 @@
 - [Trigger builds through the API](triggers/README.md)
 - [Build artifacts](../user/project/builds/artifacts.md)
 - [User permissions](../user/permissions.md#gitlab-ci)
+- [Build permissions](../user/permissions.md#build-permissions)
 - [API](../api/ci/README.md)
 - [CI services (linked docker containers)](services/README.md)
+- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
+- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 0f64137a8a9bc1c44abcb4aec24bed9abdaf7e3e..89088cf9b838eb4405a3aea4e2cbb60c51356ec1 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
 
 2. Install Docker Engine on server.
 
-    For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/).
+    For more information how to install Docker Engine on different systems
+    checkout the [Supported installations](https://docs.docker.com/engine/installation/).
 
 3. Add `gitlab-runner` user to `docker` group:
 
@@ -122,11 +123,17 @@ In order to do that, follow the steps:
         Insecure = false
     ```
 
-1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
+1. You can now use `docker` in the build script (note the inclusion of the
+   `docker:dind` service):
 
     ```yaml
     image: docker:latest
 
+    # When using dind, it's wise to use the overlayfs driver for
+    # improved performance.
+    variables:
+      DOCKER_DRIVER: overlay
+
     services:
     - docker:dind
 
@@ -140,15 +147,21 @@ In order to do that, follow the steps:
       - docker run my-docker-image /script/to/run/tests
     ```
 
-Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
-* By enabling `--docker-privileged`, you are effectively disabling all of
-the security mechanisms of containers and exposing your host to privilege
-escalation which can lead to container breakout. For more information, check out the official Docker documentation on
-[Runtime privilege and Linux capabilities][docker-cap].
-* Using docker-in-docker, each build is in a clean environment without the past
-history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
-* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
-offered.
+Docker-in-Docker works well, and is the recommended configuration, but it is
+not without its own challenges:
+
+- By enabling `--docker-privileged`, you are effectively disabling all of
+  the security mechanisms of containers and exposing your host to privilege
+  escalation which can lead to container breakout. For more information, check
+  out the official Docker documentation on
+  [Runtime privilege and Linux capabilities][docker-cap].
+- Using docker-in-docker, each build is in a clean environment without the past
+  history. Concurrent builds work fine because every build gets it's own
+  instance of Docker engine so they won't conflict with each other. But this
+  also means builds can be slower because there's no caching of layers.
+- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
+  form offered. To use a different driver, see
+  [Using the overlayfs driver](#using-the-overlayfs-driver).
 
 An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
 
@@ -188,7 +201,7 @@ In order to do that, follow the steps:
         image = "docker:latest"
         privileged = false
         disable_cache = false
-        volumes = ["/var/run/docker.sock", "/cache"]
+        volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
       [runners.cache]
         Insecure = false
     ```
@@ -221,12 +234,46 @@ work as expected since volume mounting is done in the context of the host
 machine, not the build container.
 e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
 
+## Using the OverlayFS driver
+
+By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
+copies the filesystem on every run. This is a very disk-intensive operation
+which can be avoided if a different driver is used, for example `overlay`.
+
+1. Make sure a recent kernel is used, preferably `>= 4.2`.
+1. Check whether the `overlay` module is loaded:
+
+    ```
+    sudo lsmod | grep overlay
+    ```
+
+    If you see no result, then it isn't loaded. To load it use:
+
+    ```
+    sudo modprobe overlay
+    ```
+
+    If everything went fine, you need to make sure module is loaded on reboot.
+    On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
+    following line into it:
+
+    ```
+    overlay
+    ```
+
+1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
+
+    ```
+    variables:
+      DOCKER_DRIVER: overlay
+    ```
+
 ## Using the GitLab Container Registry
 
 > **Note:**
 This feature requires GitLab 8.8 and GitLab Runner 1.2.
 
-Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../container_registry/README.md). For example, if you're using
+Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, if you're using
 docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
 
 
@@ -242,10 +289,10 @@ docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
      - docker push registry.example.com/group/project:latest
 ```
 
-You have to use the credentials of the special `gitlab-ci-token` user with its
-password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
-to your project. This allows you to automate building and deployment of your
-Docker images.
+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
+of your Docker images.
 
 Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
 including two tests that run in parallel. The build is stored in the container
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index a849905ac6b4f6a4341d8ba7f51fdca89a2dc544..aba7749091594c1d1e156c5379c80708ed0631f3 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -37,7 +37,7 @@ The registered runner will use the `ruby:2.1` docker image and will run two
 services, `postgres:latest` and `mysql:latest`, both of which will be
 accessible during the build process.
 
-## What is image
+## What is an image
 
 The `image` keyword is the name of the docker image that is present in the
 local Docker Engine (list all images with `docker images`) or any image that
@@ -47,7 +47,7 @@ Hub please read the [Docker Fundamentals][] documentation.
 In short, with `image` we refer to the docker image, which will be used to
 create a container on which your build will run.
 
-## What is service
+## What is a service
 
 The `services` keyword defines just another docker image that is run during
 your build and is linked to the docker image that the `image` keyword defines.
@@ -61,7 +61,7 @@ time the project is built.
 You can see some widely used services examples in the relevant documentation of
 [CI services examples](../services/README.md).
 
-### How is service linked to the build
+### How services are linked to the build
 
 To better understand how the container linking works, read
 [Linking containers together][linking-containers].
@@ -221,7 +221,7 @@ time.
 *Note: The following commands are run without root privileges. You should be
 able to run docker with your regular user account.*
 
-First start with creating a file named `build script`:
+First start with creating a file named `build_script`:
 
 ```bash
 cat <<EOF > build_script
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index d85b8a34cedb21f90f027ce25c1d2626ba67a51f..e070302fb826e3051456b7952d310cce8f77232f 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track
 
 Deployments are created when [jobs] deploy versions of code to [environments].
 
+### Checkout deployments locally
+
+Since 8.13, a reference in the git repository is saved for each deployment. So
+knowing what the state is of your current environments is only a `git fetch`
+away.
+
+In your git config, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
 ## Defining environments
 
 You can create and delete environments manually in the web interface, but we
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index c134106bfd0eb631d80230913b63fb42abba3b5d..ffc310ec8c719092778dd3b1544f8afbabeb8a7b 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,17 +1,21 @@
 # CI Examples
 
+A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
+If your favorite programming language or framework are missing we would love your help by sending a merge request
+with a `.gitlab-ci.yml`.
+
+Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
+
 - [Testing a PHP application](php.md)
 - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
 - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
 - [Test a Clojure application](test-clojure-application.md)
 - [Test a Scala application](test-scala-application.md)
+- [Test a Phoenix application](test-phoenix-application.md)
 - [Using `dpl` as deployment tool](deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
-  with a guide for that language.
-
-## Outside the documentation
-
+- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
 - [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
-- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
 - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml)
+
+[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
new file mode 100644
index 0000000000000000000000000000000000000000..150698ca04bfbe9cb452c8021339f77b175ba16f
--- /dev/null
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -0,0 +1,56 @@
+## Test a Phoenix application
+
+This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
+Postgres.
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+```yaml
+image: elixir:1.3
+
+services:
+  - postgres:9.6
+
+variables:
+  MIX_ENV: "test"
+
+before_script:
+  # Setup phoenix dependencies
+  - apt-get update
+  - apt-get install -y postgresql-client
+  - mix local.hex --force
+  - mix deps.get --only test
+  - mix ecto.reset
+
+test:
+  script:
+    - mix test
+```
+
+The variables will set the Mix environment to "test". The
+`before_script` will install `psql`, some Phoenix dependencies, and will also
+run your migrations.
+
+Finally, the test `script` will run your tests.
+
+### Update the Config Settings
+
+In `config/test.exs`, update the database hostname:
+
+```elixir
+config :my_app, MyApp.Repo,
+  hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
+```
+
+### Add the Migrations Folder
+
+If you do not have any migrations yet, you will need to create an empty
+`.gitkeep` file in `priv/repo/migrations`.
+
+### Sources
+
+- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
+- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index d90d7aca4fd75f17e85e029beec15d813d190b8e..7d100a4fd935701b326fd1d2a345b17739b07a3d 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -5,9 +5,9 @@ Introduced in GitLab 8.8.
 
 ## Pipelines
 
-A pipeline is a group of [builds] that get executed in [stages] (batches). All
-of the builds in a stage are executed in parallel (if there are enough
-concurrent [runners]), and if they all succeed, the pipeline moves on to the
+A pipeline is a group of [builds][] that get executed in [stages][](batches).
+All of the builds in a stage are executed in parallel (if there are enough
+concurrent [Runners]), and if they all succeed, the pipeline moves on to the
 next stage. If one of the builds fails, the next stage is not (usually)
 executed.
 
@@ -25,49 +25,22 @@ See full [documentation](yaml/README.md#jobs).
 
 ## Seeing pipeline status
 
-You can find the current and historical pipeline runs under **Pipelines** for your
-project.
+You can find the current and historical pipeline runs under **Pipelines** for
+your project.
 
 ## Seeing build status
 
 Clicking on a pipeline will show the builds that were run for that pipeline.
+Clicking on an individual build will show you its build trace, and allow you to
+cancel the build, retry it,  or erase the build trace.
 
 ## Badges
 
-There are build status and test coverage report badges available.
-
-Go to pipeline settings to see available badges and code you can use to embed
-badges in the `README.md` or your website.
-
-### Build status badge
-
-You can access a build status badge image using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/build.svg
-```
-
-### Test coverage report badge
-
-GitLab makes it possible to define the regular expression for coverage report,
-that each build log will be matched against. This means that each build in the
-pipeline can have the test coverage percentage value defined.
-
-You can access test coverage badge using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
-```
-
-If you would like to get the coverage report from the specific job, you can add
-a `job=coverage_job_name` parameter to the URL. For example, it is possible to
-use following Markdown code to embed the est coverage report into `README.md`:
-
-```markdown
-![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
-```
+Build status and test coverage report badges are available. You can find their
+respective link in the [Pipelines settings] page.
 
 [builds]: #builds
 [jobs]: yaml/README.md#jobs
 [stages]: yaml/README.md#stages
-[runners]: runners/README.md
+[runners]: runners/READM
+[pipelines settings]: ../user/project/pipelines/settings.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c835ebc2d449a2eca852061f935d3976814c1ff4..c40cdd55ea5e9127475c2557a99c0e7bf8762732 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -105,7 +105,8 @@ What is important is that each job is run independently from each other.
 
 If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
 Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-the link under **Settings > CI settings** in your project.
+a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
+**Pipelines > Builds** in your project.
 
 For more information and a complete `.gitlab-ci.yml` syntax, please read
 [the documentation on .gitlab-ci.yml](../yaml/README.md).
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 7c0fb225dac641c55c847d445c247d9fa6df5907..b858029d25e4d73f8beac71b284f18c83c247c14 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,7 +30,8 @@ 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).
+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.
 
 Then, create a new **Secret Variable** in your project settings on GitLab
 following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 6c6767fea0b14ec04031e1edb0414226aa58ca9c..84048f1d25fcd4bdf2342e142907059cd8f25cba 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -2,6 +2,10 @@
 
 > [Introduced][ci-229] in GitLab CE 7.14.
 
+> **Note**:
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
+
 Triggers can be used to force a rebuild of a specific branch, tag or commit,
 with an API call.
 
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index 2dee8ee61073b453c422fb62d78041089ed7333f..c2cf4b1852c51e760f8908b537cd8a2ba8527899 100644
Binary files a/doc/ci/triggers/img/builds_page.png and b/doc/ci/triggers/img/builds_page.png differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index baf3fc183d8ba11d78389e0aa107d0d5d1374f17..fa86f0fee3d3bdec4c4e9138621b9ab6db9d3fec 100644
Binary files a/doc/ci/triggers/img/trigger_single_build.png and b/doc/ci/triggers/img/trigger_single_build.png differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 908355c33a52103ec448176aee1d39799eb7e996..b2fcc65d304a6fcaf566aeb48919ecabcb717a92 100644
Binary files a/doc/ci/triggers/img/trigger_variables.png and b/doc/ci/triggers/img/trigger_variables.png differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 69cec5cdebfd14654fef9d3d43f126731632b37c..438f285ae2de42ca4d81de204d94cf5b52e038d3 100644
Binary files a/doc/ci/triggers/img/triggers_page.png and b/doc/ci/triggers/img/triggers_page.png differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4a7c21f811de542b1cdfdda2ce149f7bc1da9021..a4c3a731a20932b58724fcc1d793e6815bccdc48 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -34,6 +34,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
 | **CI_BUILD_REF_NAME**   | all    | all    | The branch or tag name for which project is built |
 | **CI_BUILD_REPO**       | all    | all    | The URL to clone the Git repository |
 | **CI_BUILD_TRIGGERED**  | all    | 0.5    | The flag to indicate that build was [triggered] |
+| **CI_BUILD_MANUAL**     | 8.12   | all    | The flag to indicate that build was manually started |
 | **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 |
@@ -43,10 +44,13 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
 | **CI_PROJECT_URL**      | 8.10   | 0.5    | The HTTP address to access project |
 | **CI_PROJECT_DIR**      | all    | all    | The full path where the repository is cloned and where the build is run |
 | **CI_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 returnes the address of the registry tied to the specific project |
+| **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 |
+| **GITLAB_USER_ID**      | 8.12   | all    | The id of the user who started the build |
+| **GITLAB_USER_EMAIL**   | 8.12   | all    | The email of the user who started the build |
 
 **Some of the variables are only available when using runner with at least defined version.**
 
@@ -60,6 +64,7 @@ export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@gitlab.com/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"
@@ -76,8 +81,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="8.9.0"
-export CI_SERVER_VERSION="70606bf"
+export CI_SERVER_REVISION="70606bf"
+export CI_SERVER_VERSION="8.9.0"
+export GITLAB_USER_ID="42"
+export GITLAB_USER_EMAIL="alexzander@sporer.com"
 ```
 
 ### YAML-defined variables
@@ -99,6 +106,39 @@ Variables can be defined at a global level, but also at a job level.
 
 More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
 
+#### Debug tracing
+
+> **WARNING:** Enabling debug tracing can have severe security implications. The
+  output **will** contain the content of all your secure variables and any other
+  secrets! The output **will** be uploaded to the GitLab server and made visible
+  in build traces!
+
+By default, GitLab Runner hides most of the details of what it is doing when
+processing a job. This behaviour keeps build traces short, and prevents secrets
+from being leaked into the trace unless your script writes them to the screen.
+
+If a job isn't working as expected, this can make the problem difficult to
+investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
+Available on GitLab Runner v1.7+, this feature enables the shell's execution
+trace, resulting in a verbose build trace listing all commands that were run,
+variables that were set, etc.
+
+Before enabling this, you should ensure builds are visible to
+[team members only](../../../user/permissions.md#project-features). You should
+also [erase](../pipelines.md#seeing-build-traces) all generated build traces
+before making them visible again.
+
+To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
+
+```yaml
+job1:
+  variables:
+    CI_DEBUG_TRACE: "true"
+```
+
+The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace)
+demonstrates a working configuration, including build trace examples.
+
 ### User-defined variables (Secure Variables)
 **This feature requires GitLab Runner 0.4.0 or higher**
 
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 01d7108854398bbaf676d2026a587770216d22ea..5c0e1c44e3fc5970d0f40f043933e3bdc115a34a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -6,50 +6,6 @@ GitLab Runner to manage your project's builds.
 If you want a quick introduction to GitLab CI, follow our
 [quick start guide](../quick_start/README.md).
 
----
-
-<!-- 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)*
-
-- [.gitlab-ci.yml](#gitlab-ci-yml)
-    - [image and services](#image-and-services)
-    - [before_script](#before_script)
-    - [after_script](#after_script)
-    - [stages](#stages)
-    - [types](#types)
-    - [variables](#variables)
-    - [cache](#cache)
-        - [cache:key](#cache-key)
-- [Jobs](#jobs)
-    - [script](#script)
-    - [stage](#stage)
-    - [only and except](#only-and-except)
-    - [job variables](#job-variables)
-    - [tags](#tags)
-    - [allow_failure](#allow_failure)
-    - [when](#when)
-        - [Manual actions](#manual-actions)
-    - [environment](#environment)
-    - [artifacts](#artifacts)
-        - [artifacts:name](#artifacts-name)
-        - [artifacts:when](#artifacts-when)
-        - [artifacts:expire_in](#artifacts-expire_in)
-    - [dependencies](#dependencies)
-    - [before_script and after_script](#before_script-and-after_script)
-- [Git Strategy](#git-strategy)
-- [Shallow cloning](#shallow-cloning)
-- [Hidden jobs](#hidden-jobs)
-- [Special YAML features](#special-yaml-features)
-    - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
-- [Skipping builds](#skipping-builds)
-- [Examples](#examples)
-
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
----
-
 ## .gitlab-ci.yml
 
 From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
@@ -134,8 +90,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
 
 ### after_script
 
->**Note:**
-Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
+> Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
 
 `after_script` is used to define the command that will be run after for all
 builds. This has to be an array or a multi-line string.
@@ -179,11 +134,10 @@ Alias for [stages](#stages).
 
 ### variables
 
->**Note:**
-Introduced in GitLab Runner v0.5.0.
+> Introduced in GitLab Runner v0.5.0.
 
 GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
-build environment. The variables are stored in the git repository and are meant
+build environment. The variables are stored in the Git repository and are meant
 to store non-sensitive project configuration, for example:
 
 ```yaml
@@ -192,19 +146,25 @@ variables:
 ```
 
 These variables can be later used in all executed commands and scripts.
-
 The YAML-defined variables are also set to all created service containers,
-thus allowing to fine tune them.
+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
+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.
 
-Variables can be also defined on [job level](#job-variables).
+[Learn more about variables.][variables]
 
 ### cache
 
->**Note:**
-Introduced in GitLab Runner v0.7.0.
+> Introduced in GitLab Runner v0.7.0.
 
 `cache` is used to specify a list of files and directories which should be
-cached between builds.
+cached between builds. You can only use paths that are within the project
+workspace.
 
 **By default the caching is enabled per-job and per-branch.**
 
@@ -262,8 +222,7 @@ will be always present. For implementation details, please check GitLab Runner.
 
 #### cache:key
 
->**Note:**
-Introduced in GitLab Runner v1.0.0.
+> Introduced in GitLab Runner v1.0.0.
 
 The `key` directive allows you to define the affinity of caching
 between jobs, allowing to have a single cache for all jobs,
@@ -353,7 +312,7 @@ job_name:
 | except        | no | Defines a list of git refs for which build is not created |
 | tags          | no | Defines a list of tags which are used to select Runner |
 | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
-| when          | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| when          | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
 | dependencies  | no | Define other builds that a build depends on so that you can pass artifacts between them|
 | artifacts     | no | Define list of build artifacts |
 | cache         | no | Define list of files that should be cached between subsequent runs |
@@ -573,8 +532,7 @@ The above script will:
 
 #### Manual actions
 
->**Note:**
-Introduced in GitLab 8.10.
+> Introduced in GitLab 8.10.
 
 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
@@ -585,23 +543,31 @@ An example usage of manual actions is deployment to production.
 
 ### environment
 
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
 
-`environment` is used to define that a job deploys to a specific environment.
-This allows easy tracking of all deployments to your environments straight from
-GitLab.
+> You can read more about environments and find more examples in the
+[documentation about environments][environment].
 
+`environment` is used to define that a job deploys to a specific environment.
 If `environment` is specified and no environment under that name exists, a new
 one will be created automatically.
 
-The `environment` name must contain only letters, digits, '-' and '_'. Common
-names are `qa`, `staging`, and `production`, but you can use whatever name works
-with your workflow.
+The `environment` name can contain:
 
----
+- letters
+- digits
+- spaces
+- `-`
+- `_`
+- `/`
+- `$`
+- `{`
+- `}`
 
-**Example configurations**
+Common names are `qa`, `staging`, and `production`, but you can use whatever
+name works with your workflow.
+
+In its simplest form, the `environment` keyword can be defined like:
 
 ```
 deploy to production:
@@ -610,8 +576,134 @@ deploy to production:
   environment: production
 ```
 
-The `deploy to production` job will be marked as doing deployment to
-`production` environment.
+In the above example, the `deploy to production` job will be marked as doing a
+deployment to the `production` environment.
+
+#### environment:name
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the name of an environment could be defined as a string like
+`environment: production`. The recommended way now is to define it under the
+`name` keyword.
+
+Instead of defining the name of the environment right after the `environment`
+keyword, it is also possible to define it as a separate value. For that, use
+the `name` keyword under `environment`:
+
+```
+deploy to production:
+  stage: deploy
+  script: git push production HEAD:master
+  environment:
+    name: production
+```
+
+#### environment:url
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the URL could be added only in GitLab's UI. The
+recommended way now is to define it in `.gitlab-ci.yml`.
+
+This is an optional value that when set, it exposes buttons in various places
+in GitLab which when clicked take you to the defined URL.
+
+In the example below, if the job finishes successfully, it will create buttons
+in the merge requests and in the environments/deployments pages which will point
+to `https://prod.example.com`.
+
+```
+deploy to production:
+  stage: deploy
+  script: git push production HEAD:master
+  environment:
+    name: production
+    url: https://prod.example.com
+```
+
+#### environment:on_stop
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
+`environment`. It declares a different job that runs in order to close
+the environment.
+
+Read the `environment:action` section for an example.
+
+#### environment:action
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+The `action` keyword is to be used in conjunction with `on_stop` and is defined
+in the job that is called to close the environment.
+
+Take for instance:
+
+```yaml
+review_app:
+  stage: deploy
+  script: make deploy-app
+  environment:
+    name: review
+    on_stop: stop_review_app
+
+stop_review_app:
+  stage: deploy
+  script: make delete-app
+  when: manual
+  environment:
+    name: review
+    action: stop
+```
+
+In the above example we set up the `review_app` job to deploy to the `review`
+environment, and we also defined a new `stop_review_app` job under `on_stop`.
+Once the `review_app` job is successfully finished, it will trigger the
+`stop_review_app` job based on what is defined under `when`. In this case we
+set it up to `manual` so it will need a [manual action](#manual-actions) via
+GitLab's web interface in order to run.
+
+The `stop_review_app` job is **required** to have the following keywords defined:
+
+- `when` - [reference](#when)
+- `environment:name`
+- `environment:action`
+
+#### dynamic environments
+
+> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+
+`environment` can also represent a configuration hash with `name` and `url`.
+These parameters can use any of the defined [CI variables](#variables)
+(including predefined, secure variables and `.gitlab-ci.yml` variables).
+
+For example:
+
+```
+deploy as review app:
+  stage: deploy
+  script: make deploy
+  environment:
+    name: review-apps/$CI_BUILD_REF_NAME
+    url: https://$CI_BUILD_REF_NAME.review.example.com/
+```
+
+The `deploy as review app` job will be marked as deployment to dynamically
+create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. If for example the
+`deploy as review app` job was run in a branch named `pow`, this environment
+should be accessible under `https://pow.review.example.com/`.
+
+This of course implies that the underlying server which hosts the application
+is properly configured.
+
+The common use case is to create dynamic environments for branches and use them
+as Review Apps. You can see a simple example using Review Apps at
+https://gitlab.com/gitlab-examples/review-apps-nginx/.
 
 ### artifacts
 
@@ -623,8 +715,8 @@ The `deploy to production` job will be marked as doing deployment to
 > - Build artifacts are only collected for successful builds by default.
 
 `artifacts` is used to specify a list of files and directories which should be
-attached to the build after success. To pass artifacts between different builds,
-see [dependencies](#dependencies).
+attached to the build after success. You can only use paths that are within the
+project workspace. To pass artifacts between different builds, see [dependencies](#dependencies).
 
 Below are some examples.
 
@@ -680,8 +772,7 @@ be available for download in the GitLab UI.
 
 #### artifacts:name
 
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
 
 The `name` directive allows you to define the name of the created artifacts
 archive. That way, you can have a unique name for every archive which could be
@@ -744,8 +835,7 @@ job:
 
 #### artifacts:when
 
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
 
 `artifacts:when` is used to upload artifacts on build failure or despite the
 failure.
@@ -770,8 +860,7 @@ job:
 
 #### artifacts:expire_in
 
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
 
 `artifacts:expire_in` is used to delete uploaded artifacts after the specified
 time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
@@ -806,8 +895,7 @@ job:
 
 ### dependencies
 
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
 
 This feature should be used in conjunction with [`artifacts`](#artifacts) and
 allows you to define the artifacts to pass between different builds.
@@ -881,32 +969,48 @@ job:
 
 ## Git Strategy
 
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
-releases or be removed completely.
+> Introduced in GitLab 8.9 as an experimental feature.  May change or be removed
+  completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner
+  v1.7+.
 
-You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
-is slower, but makes sure you have a clean directory before every build. `fetch`
-is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
-in the `variables` section for individual jobs. If it's not specified, then the
-default from project settings will be used.
+You can set the `GIT_STRATEGY` used for getting recent application code, either
+in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs. If left unspecified, the default from project
+settings will be used.
+
+There are three possible values: `clone`, `fetch`, and `none`.
+
+`clone` is the slowest option. It clones the repository from scratch for every
+job, ensuring that the project workspace is always pristine.
 
 ```
 variables:
   GIT_STRATEGY: clone
 ```
 
-or
+`fetch` is faster as it re-uses the project workspace (falling back to `clone`
+if it doesn't exist). `git clean` is used to undo any changes made by the last
+job, and `git fetch` is used to retrieve commits made since the last job ran.
 
 ```
 variables:
   GIT_STRATEGY: fetch
 ```
 
+`none` also re-uses the project workspace, but skips all Git operations
+(including GitLab Runner's pre-clone script, if present). It is mostly useful
+for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
+data may be present, but it is certain to be out of date, so you should only
+rely on files brought into the project workspace from cache or artifacts.
+
+```
+variables:
+  GIT_STRATEGY: none
+```
+
 ## Shallow cloning
 
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
+> Introduced in GitLab 8.9 as an experimental feature. May change in future
 releases or be removed completely.
 
 You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
@@ -934,24 +1038,26 @@ variables:
   GIT_DEPTH: "3"
 ```
 
-## Hidden jobs
+## Hidden keys
 
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
 
-Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
 use this feature to ignore jobs, or use the
-[special YAML features](#special-yaml-features) and transform the hidden jobs
+[special YAML features](#special-yaml-features) and transform the hidden keys
 into templates.
 
-In the following example, `.job_name` will be ignored:
+In the following example, `.key_name` will be ignored:
 
 ```yaml
-.job_name:
+.key_name:
   script:
     - rake spec
 ```
 
+Hidden keys can be hashes like normal CI jobs, but you are also allowed to use
+different types of structures to leverage special YAML features.
+
 ## Special YAML features
 
 It's possible to use special YAML features like anchors (`&`), aliases (`*`)
@@ -962,12 +1068,11 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
 
 ### Anchors
 
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
 
 YAML also has a handy feature called 'anchors', which let you easily duplicate
 content across your document. Anchors can be used to duplicate/inherit
-properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
 to provide templates for your jobs.
 
 The following example uses anchors and map merging. It will create two jobs,
@@ -975,7 +1080,7 @@ The following example uses anchors and map merging. It will create two jobs,
 having their own custom `script` defined:
 
 ```yaml
-.job_template: &job_definition  # Hidden job that defines an anchor named 'job_definition'
+.job_template: &job_definition  # Hidden key that defines an anchor named 'job_definition'
   image: ruby:2.1
   services:
     - postgres
@@ -1081,7 +1186,14 @@ test:mysql:
     - ruby
 ```
 
-You can see that the hidden jobs are conveniently used as templates.
+You can see that the hidden keys are conveniently used as templates.
+
+## Triggers
+
+Triggers can be used to force a rebuild of a specific branch, tag or commit,
+with an API call.
+
+[Read more in the triggers documentation.](../triggers/README.md)
 
 ## Validate the .gitlab-ci.yml
 
@@ -1099,3 +1211,7 @@ Visit the [examples README][examples] to see a list of examples using GitLab
 CI with various languages.
 
 [examples]: ../examples/README.md
+[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
+[environment]: ../environments.md
+[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
+[variables]: ../variables/README.md
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 047a0b08406941bcea0002b90f9fd39437c5ea59..fe3e4681ba7c824b21fd1bfd740750e4ee14b5a2 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,98 +1 @@
-# GitLab Container Registry
-
-> [Introduced][ce-4040] in GitLab 8.8. Docker Registry manifest
-`v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10.
-
-> **Note:**
-This document is about the user guide. To learn how to enable GitLab Container
-Registry across your GitLab instance, visit the
-[administrator documentation](../administration/container_registry.md).
-
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
----
-
-## Enable the Container Registry for your project
-
-1. First, ask your system administrator to enable GitLab Container Registry
-   following the [administration documentation](../administration/container_registry.md).
-   If you are using GitLab.com, this is enabled by default so you can start using
-   the Registry immediately.
-
-1. Go to your project's settings and enable the **Container Registry** feature
-   on your project. For new projects this might be enabled by default. For
-   existing projects you will have to explicitly enable it.
-
-    ![Enable Container Registry](img/project_feature.png)
-
-## Build and push images
-
-After you save your project's settings, you should see a new link in the
-sidebar called **Container Registry**. Following this link will get you to
-your project's Registry panel where you can see how to login to the Container
-Registry using your GitLab credentials.
-
-For example if the Registry's URL is `registry.example.com`, the you should be
-able to login with:
-
-```
-docker login registry.example.com
-```
-
-Building and publishing images should be a straightforward process. Just make
-sure that you are using the Registry URL with the namespace and project name
-that is hosted on GitLab:
-
-```
-docker build -t registry.example.com/group/project .
-docker push registry.example.com/group/project
-```
-
-## Use images from GitLab Container Registry
-
-To download and run a container from images hosted in GitLab Container Registry,
-use `docker run`:
-
-```
-docker run [options] registry.example.com/group/project [arguments]
-```
-
-For more information on running Docker containers, visit the
-[Docker documentation][docker-docs].
-
-## Control Container Registry from within GitLab
-
-GitLab offers a simple Container Registry management panel. Go to your project
-and click **Container Registry** in the left sidebar.
-
-This view will show you all tags in your project and will easily allow you to
-delete them.
-
-![Container Registry panel](img/container_registry.png)
-
-## Build and push images using GitLab CI
-
-> **Note:**
-This feature requires GitLab 8.8 and GitLab Runner 1.2.
-
-Make sure that your GitLab Runner is configured to allow building docker images.
-You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
-Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
-
-## Limitations
-
-In order to use a container image from your private project as an `image:` in
-your `.gitlab-ci.yml`, you have to follow the
-[Using a private Docker Registry][private-docker]
-documentation. This workflow will be simplified in the future.
-
-## Troubleshooting
-
-See [the GitLab Docker registry troubleshooting guide](troubleshooting.md).
-
-[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
-[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+This document was moved in [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png
deleted file mode 100644
index 57d6f9f22c584d603d9393034dd7f24ab637b7a1..0000000000000000000000000000000000000000
Binary files a/doc/container_registry/img/container_registry.png and /dev/null differ
diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png
deleted file mode 100644
index a59b4f82b561683642523ab735392b5249dc9bdd..0000000000000000000000000000000000000000
Binary files a/doc/container_registry/img/project_feature.png and /dev/null differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
index 14c4a7d9a63ee99aafc3754dbb9c83378a76c86f..2f8cd37b488f7c5c39b16015cfbf5b159493acbe 100644
--- a/doc/container_registry/troubleshooting.md
+++ b/doc/container_registry/troubleshooting.md
@@ -1,141 +1 @@
-# Troubleshooting the GitLab Container Registry
-
-## Basic Troubleshooting
-
-1. Check to make sure that the system clock on your Docker client and GitLab server have
-   been synchronized (e.g. via NTP).
-
-2. If you are using an S3-backed Registry, double check that the IAM
-   permissions and the S3 credentials (including region) are correct. See [the
-   sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
-   for more details.
-
-3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
-   for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
-   there.
-
-## Advanced Troubleshooting
-
->**NOTE:** The following section is only recommended for experts.
-
-Sometimes it's not obvious what is wrong, and you may need to dive deeper into
-the communication between the Docker client and the Registry to find out
-what's wrong. We will use a concrete example in the past to illustrate how to
-diagnose a problem with the S3 setup.
-
-### Unexpected 403 error during push
-
-A user attempted to enable an S3-backed Registry. The `docker login` step went
-fine. However, when pushing an image, the output showed:
-
-```
-The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
-dc5e59c14160: Pushing [==================================================>] 14.85 kB
-03c20c1a019a: Pushing [==================================================>] 2.048 kB
-a08f14ef632e: Pushing [==================================================>] 2.048 kB
-228950524c88: Pushing 2.048 kB
-6a8ecde4cc03: Pushing [==>                                                ] 9.901 MB/205.7 MB
-5f70bf18a086: Pushing 1.024 kB
-737f40e80b7f: Waiting
-82b57dbc5385: Waiting
-19429b698a22: Waiting
-9436069b92a3: Waiting
-error parsing HTTP 403 response body: unexpected end of JSON input: ""
-```
-
-This error is ambiguous, as it's not clear whether the 403 is coming from the
-GitLab Rails application, the Docker Registry, or something else. In this
-case, since we know that since the login succeeded, we probably need to look
-at the communication between the client and the Registry.
-
-The REST API between the Docker client and Registry is [described
-here](https://docs.docker.com/registry/spec/api/). Normally, one would just
-use Wireshark or tcpdump to capture the traffic and see where things went
-wrong.  However, since all communication between Docker clients and servers
-are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
-if you know the private key. What can we do instead?
-
-One way would be to disable HTTPS by setting up an [insecure
-Registry](https://docs.docker.com/registry/insecure/). This could introduce a
-security hole and is only recommended for local testing. If you have a
-production system and can't or don't want to do this, there is another way:
-use mitmproxy, which stands for Man-in-the-Middle Proxy.
-
-### mitmproxy
-
-[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
-client and server to inspect all traffic. One wrinkle is that your system
-needs to trust the mitmproxy SSL certificates for this to work.
-
-The following installation instructions assume you are running Ubuntu:
-
-1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
-1. Run `mitmproxy --port 9000` to generate its certificates.
-   Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
-1. Install the certificate from `~/.mitmproxy` to your system:
-
-    ```sh
-    sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
-    sudo update-ca-certificates
-    ```
-
-If successful, the output should indicate that a certificate was added:
-
-```sh
-Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
-Running hooks in /etc/ca-certificates/update.d....done.
-```
-
-To verify that the certificates are properly installed, run:
-
-```sh
-mitmproxy --port 9000
-```
-
-This will run mitmproxy on port `9000`. In another window, run:
-
-```sh
-curl --proxy http://localhost:9000 https://httpbin.org/status/200
-```
-
-If everything is setup correctly, you will see information on the mitmproxy window and
-no errors from the curl commands.
-
-### Running the Docker daemon with a proxy
-
-For Docker to connect through a proxy, you must start the Docker daemon with the
-proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
-and then run Docker by hand. As root, run:
-
-```sh
-export HTTP_PROXY="http://localhost:9000"
-export HTTPS_PROXY="https://localhost:9000"
-docker daemon --debug
-```
-
-This will launch the Docker daemon and proxy all connections through mitmproxy.
-
-### Running the Docker client
-
-Now that we have mitmproxy and Docker running, we can attempt to login and push
-a container image. You may need to run as root to do this. For example:
-
-```sh
-docker login s3-testing.myregistry.com:4567
-docker push s3-testing.myregistry.com:4567/root/docker-test
-```
-
-In the example above, we see the following trace on the mitmproxy window:
-
-![mitmproxy output from Docker](img/mitmproxy-docker.png)
-
-The above image shows:
-
-* The initial PUT requests went through fine with a 201 status code.
-* The 201 redirected the client to the S3 bucket.
-* The HEAD request to the AWS bucket reported a 403 Unauthorized.
-
-What does this mean? This strongly suggests that the S3 user does not have the right
-[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
-The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
-Once the right permissions were set, the error will go away.
+This document was moved to [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 4620bb2dcde68372f9ce052c381faa94b63c3a9a..31164ccd465633457d454d2f6838fb4c9c3af0ef 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,39 +1,4 @@
-# Issue closing pattern
+This document was split into:
 
-When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch.
-
-If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from
-the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there.
-
-When not specified, the default `issue_closing_pattern` as shown below will be used:
-
-```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
-```
-
-Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`).
-
-For example:
-
-```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
-```
-
-will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
-
-Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
-to test your own patterns.
-Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`.
-
-## Change the pattern
-
-For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
-
-```
-issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
-```
-
-For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key.
-
-[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
-[1]: http://rubular.com/r/Xmbexed1OJ
+- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
+- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 57f37da6f809307e262e39dc16da2d6c9a55247b..bf1f054b7d5fa5aed1dc2085f60e97df95b56e20 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -8,16 +8,23 @@
 
 ## Styleguides
 
+- [API styleguide](api_styleguide.md) Use this styleguide if you are
+  contributing to the API.
 - [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
   contributing to documentation.
 - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
 - [Testing standards and style guidelines](testing.md)
 - [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
-- [SQL guidelines](sql.md) for SQL guidelines
+- [Frontend guidelines](frontend.md)
+- [SQL guidelines](sql.md) for working with SQL queries
+- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
 
 ## Process
 
+- [Generate a changelog entry with `bin/changelog`](changelog.md)
 - [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
+- [Merge request performance guidelines](merge_request_performance_guidelines.md)
+  for ensuring merge requests do not negatively impact GitLab performance
 
 ## Backend howtos
 
@@ -35,6 +42,7 @@
 
 - [What requires downtime?](what_requires_downtime.md)
 - [Adding database indexes](adding_database_indexes.md)
+- [Post Deployment Migrations](post_deployment_migrations.md)
 
 ## Compliance
 
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
new file mode 100644
index 0000000000000000000000000000000000000000..ce444ebdde4199913e36a0bd1bbf822a40d3c4bd
--- /dev/null
+++ b/doc/development/api_styleguide.md
@@ -0,0 +1,96 @@
+# API styleguide
+
+This styleguide recommends best practices for API development.
+
+## Instance variables
+
+Please do not use instance variables, there is no need for them (we don't need
+to access them as we do in Rails views), local variables are fine.
+
+## Entities
+
+Always use an [Entity] to present the endpoint's payload.
+
+## Methods and parameters description
+
+Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods)
+(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
+for a good example):
+
+- `desc` for the method summary. You should pass it a block for additional
+  details such as:
+  - The GitLab version when the endpoint was added
+  - If the endpoint is deprecated, and if so, when will it be removed
+
+- `params` for the method params. This acts as description,
+  [validation, and coercion of the parameters]
+
+A good example is as follows:
+
+```ruby
+desc 'Get all broadcast messages' do
+  detail 'This feature was introduced in GitLab 8.12.'
+  success Entities::BroadcastMessage
+end
+params do
+  optional :page,     type: Integer, desc: 'Current page number'
+  optional :per_page, type: Integer, desc: 'Number of messages per page'
+end
+get do
+  messages = BroadcastMessage.all
+
+  present paginate(messages), with: Entities::BroadcastMessage
+end
+```
+
+## Declared params
+
+> Grape allows you to access only the parameters that have been declared by your
+`params` block. It filters out the params that have been passed, but are not
+allowed.
+
+– https://github.com/ruby-grape/grape#declared
+
+### Exclude params from parent namespaces!
+
+> By default `declared(params) `includes parameters that were defined in all
+parent namespaces.
+
+– https://github.com/ruby-grape/grape#include-parent-namespaces
+
+In most cases you will want to exclude params from the parent namespaces:
+
+```ruby
+declared(params, include_parent_namespaces: false)
+```
+
+### When to use `declared(params)`?
+
+You should always use `declared(params)` when you pass the params hash as
+arguments to a method call.
+
+For instance:
+
+```ruby
+# bad
+User.create(params) # imagine the user submitted `admin=1`... :)
+
+# good
+User.create(declared(params, include_parent_namespaces: false).to_h)
+```
+
+>**Note:**
+`declared(params)` return a `Hashie::Mash` object, on which you will have to
+call `.to_h`.
+
+But we can use `params[key]` directly when we access single elements.
+
+For instance:
+
+```ruby
+# good
+Model.create(foo: params[:foo])
+```
+
+[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb
+[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
new file mode 100644
index 0000000000000000000000000000000000000000..6a97fae9cac9ca287b84114e39704ce1bd3503a5
--- /dev/null
+++ b/doc/development/changelog.md
@@ -0,0 +1,185 @@
+# Generate a changelog entry
+
+This guide contains instructions for generating a changelog entry data file, as
+well as information and history about our changelog process.
+
+## Overview
+
+Each bullet point, or **entry**, in our [`CHANGELOG.md`][changelog.md] file is
+generated from a single data file in the [`changelogs/unreleased/`][unreleased]
+(or corresponding EE) folder. The file is expected to be a [YAML] file in the
+following format:
+
+```yaml
+---
+title: "Going through change[log]s"
+merge_request: 1972
+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.
+
+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.
+
+[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
+
+A `bin/changelog` script is available to generate the changelog entry file
+automatically.
+
+Its simplest usage is to provide the value for `title`:
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+create changelogs/unreleased/my-feature.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request:
+author:
+```
+
+The entry filename is based on the name of the current Git branch. If you run
+the command above on a branch called `feature/hey-dz`, it will generate a
+`changelogs/unreleased/feature-hey-dz.yml` file.
+
+### Arguments
+
+| 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`
+
+You can pass the **`--amend`** argument to automatically stage the generated
+file and amend it to the previous commit.
+
+If you use **`--amend`** and don't provide a title, it will automatically use
+the "subject" of the previous commit, which is the first line of the commit
+message:
+
+```text
+$ git show --oneline
+ab88683 Added an awesome new feature to GitLab
+
+$ bin/changelog --amend
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Added an awesome new feature to GitLab
+merge_request:
+author:
+```
+
+#### `--force` or `-f`
+
+Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
+already exists.
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+error changelogs/unreleased/feature-hey-dz.yml already exists! Use `--force` to overwrite.
+
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!' --force
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request: 1983
+author:
+```
+
+#### `--merge-request` or `-m`
+
+Use the **`--merge-request`** or **`-m`** argument to provide the
+`merge_request` value:
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request: 1983
+author:
+```
+
+#### `--dry-run` or `-n`
+
+Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
+committing anything:
+
+```text
+$ bin/changelog --amend --dry-run
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Added an awesome new feature to GitLab
+merge_request:
+author:
+
+$ ls changelogs/unreleased/
+```
+
+#### `--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:
+
+```text
+$ git config user.name
+Jane Doe
+
+$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!'
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request:
+author: Jane Doe
+```
+
+## 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
+entries at the same spot in the list, it created a merge conflict in one as soon
+as the other was merged. When we had dozens of merge requests fighting for the
+same changelog entry location, this quickly became a major source of merge
+conflicts and delays in development.
+
+This led us to a [boring solution] of "add your entry in a random location in
+the list." This actually worked pretty well as we got further along in each
+monthly release cycle, but at the start of a new cycle, when a new version
+section was added and there were fewer places to "randomly" add an entry, the
+conflicts became a problem again until we had a sufficient number of entries.
+
+On top of all this, it created an entirely different headache for [release managers]
+when they cherry-picked a commit into a stable branch for a patch release. If
+the commit included an entry in the `CHANGELOG`, it would include the entire
+changelog for the latest version in `master`, so the release manager would have
+to manually remove the later entries. They often would have had to do this
+multiple times per patch release. This was compounded when we had to release
+multiple patches at once due to a security issue.
+
+We needed to automate all of this manual work. So we [started brainstorming].
+After much discussion we settled on the current solution of one file per entry,
+and then compiling the entries into the overall `CHANGELOG.md` file during the
+[release process].
+
+[boring solution]: https://about.gitlab.com/handbook/#boring-solutions
+[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md
+[started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826
+[release process]: https://gitlab.com/gitlab-org/release-tools
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 40ae55ab905527733b6c962d3db4b8a9f2b3d269..c5c23b5c0b813bc63884c6eadfa25e17780ce308 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -34,6 +34,10 @@ request is up to one of our merge request "endbosses", denoted on the
 
 ## Having your code reviewed
 
+Please keep in mind that code review is a process that can take multiple
+iterations, and reviewers may spot things later that they may not have seen the
+first time.
+
 - The first reviewer of your code is _you_. Before you perform that first push
   of your shiny new branch, read through the entire diff. Does it make sense?
   Did you include something unrelated to the overall purpose of the changes? Did
@@ -55,6 +59,7 @@ request is up to one of our merge request "endbosses", denoted on the
 Understand why the change is necessary (fixes a bug, improves the user
 experience, refactors the existing code). Then:
 
+- Try to be thorough in your reviews to reduce the number of iterations.
 - Communicate which ideas you feel strongly about and those you don't.
 - Identify ways to simplify the code while still solving the problem.
 - Offer alternative implementations, but assume the author already considered
@@ -64,8 +69,10 @@ experience, refactors the existing code). Then:
   someone else would be confused by it as well.
 - After a round of line notes, it can be helpful to post a summary note such as
   "LGTM :thumbsup:", or "Just a couple things to address."
-- Avoid accepting a merge request before the build succeeds ("Merge when build
-  succeeds" is fine).
+- Avoid accepting a merge request before the build succeeds. Of course, "Merge
+  When Build Succeeds" (MWBS) is fine.
+- If you set the MR to "Merge When Build Succeeds", you should take over
+  subsequent revisions for anything that would be spotted after that.
 
 ## Credits
 
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 927a18724135411c679050b6a36f7cba28004b12..b137e6ae82e98e0c02a7c72d84481da2ce44e5d6 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -6,7 +6,7 @@ it organized and easy to find.
 ## Location and naming of documents
 
 >**Note:**
-These guidelines derive from the discussion taken place in issue [#3349](ce-3349).
+These guidelines derive from the discussion taken place in issue [#3349][ce-3349].
 
 The documentation hierarchy can be vastly improved by providing a better layout
 and organization of directories.
@@ -93,12 +93,14 @@ merge request.
   links shift too, which eventually leads to dead links. If you think it is
   compelling to add numbers in headings, make sure to at least discuss it with
   someone in the Merge Request
+- Avoid adding things that show ephemeral statuses. For example, if a feature is
+  considered beta or experimental, put this info in a note, not in the heading.
 - When introducing a new document, be careful for the headings to be
   grammatically and syntactically correct. It is advised to mention one or all
-  of the following GitLab members for a review: `@axil`, `@rspeicher`,
-  `@dblessing`, `@ashleys`. This is to ensure that no document
-  with wrong heading is going live without an audit, thus preventing dead links
-  and redirection issues when corrected
+  of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`,
+  `@SeanPackham`. This is to ensure that no document with wrong heading is going
+  live without an audit, thus preventing dead links and redirection issues when
+  corrected
 - Leave exactly one newline after a heading
 
 ## Links
@@ -155,15 +157,30 @@ Inside the document:
 
 - Every piece of documentation that comes with a new feature should declare the
   GitLab version that feature got introduced. Right below the heading add a
-  note: `> Introduced in GitLab 8.3.`.
+  note:
+
+    ```
+    > Introduced in GitLab 8.3.
+    ```
+
 - If possible every feature should have a link to the MR that introduced it.
   The above note would be then transformed to:
-  `> [Introduced][ce-1242] in GitLab 8.3.`, where
-  the [link identifier](#links) is named after the repository (CE) and the MR
-  number.
-- If the feature is only in GitLab EE, don't forget to mention it, like:
-  `> Introduced in GitLab EE 8.3.`. Otherwise, leave
-  this mention out.
+
+    ```
+    > [Introduced][ce-1242] in GitLab 8.3.
+    ```
+
+    , where the [link identifier](#links) is named after the repository (CE) and
+    the MR number.
+
+- If the feature is only in GitLab Enterprise Edition, don't forget to mention
+  it, like:
+
+    ```
+    > Introduced in GitLab Enterprise Edition 8.3.
+    ```
+
+    Otherwise, leave this mention out.
 
 ## References
 
@@ -222,18 +239,26 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
     ```
 
 1. Find and replace any occurrences of the old location with the new one.
-   A quick way to find them is to use `grep`:
+   A quick way to find them is to use `git grep`. First go to the root directory
+   where you cloned the `gitlab-ce` repository and then do:
 
     ```
-    grep -nR "lfs_administration.md" doc/
+    git grep -n "workflow/lfs/lfs_administration"
+    git grep -n "lfs/lfs_administration"
     ```
 
-    The above command will search in the `doc/` directory for
-    `lfs_administration.md` recursively and will print the file and the line
-    where this file is mentioned. Note that we used just the filename
-    (`lfs_administration.md`) and not the whole the relative path
-    (`workflow/lfs/lfs_administration.md`).
+Things to note:
 
+- Since we also use inline documentation, except for the documentation itself,
+  the document might also be referenced in the views of GitLab (`app/`) which will
+  render when visiting `/help`, and sometimes in the testing suite (`spec/`).
+- The above `git grep` command will search recursively in the directory you run
+  it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
+  and will print the file and the line where this file is mentioned.
+  You may ask why the two greps. Since we use relative paths to link to
+  documentation, sometimes it might be useful to search a path deeper.
+- The `*.md` extension is not used when a document is linked to GitLab's
+  built-in help page, that's why we omit it in `git grep`.
 
 ## Configuration documentation for source and Omnibus installations
 
@@ -291,17 +316,34 @@ In this case:
 - different highlighting languages are used for each config in the code block
 - the [references](#references) guide is used for reconfigure/restart
 
+## Fake tokens
+
+There may be times where a token is needed to demonstrate an API call using
+cURL or a secret variable used in CI. It is strongly advised not to use real
+tokens in documentation even if the probability of a token being exploited is
+low.
+
+You can use the following fake tokens as examples.
+
+|     **Token type**    |           **Token value**         |
+| --------------------- | --------------------------------- |
+| Private user token    | `9koXpg98eAheJpvBs5tK`            |
+| Personal access token | `n671WNGecHugsdEDPsyo`            |
+| Application ID        | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
+| Application secret    | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
+| Secret CI variable    | `Li8j-mLUVA3eZYjPfd_H`            |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd`        |
+| Shared Runner token   | `6Vk7ZsosqQyfreAxXTZr`            |
+| Trigger token         | `be20d8dcc028677c931e04f3871a9b`  |
+| Webhook secret token  | `6XhDroRcYPM5by_h-HLY`            |
+| Health check token    | `Tu7BgjR9qeZTEyRzGG2P`            |
+| Request profile token | `7VgpS4Ax5utVD2esNstz`            |
+
 ## API
 
 Here is a list of must-have items. Use them in the exact order that appears
 on this document. Further explanation is given below.
 
-- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods)
-  (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
-  for a good example):
-  - `desc` for the method summary (you can pass it a block for additional details)
-  - `params` for the method params (this acts as description **and** validation
-    of the params)
 - Every method must have the REST API request. For example:
 
     ```
@@ -422,7 +464,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
 
 [cURL]: http://curl.haxx.se/ "cURL website"
 [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
-[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
+[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
+[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242
 [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
 [ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
 [graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
new file mode 100644
index 0000000000000000000000000000000000000000..ec8f2d6531c9f4d3f4f4f441d36917899d3146f4
--- /dev/null
+++ b/doc/development/frontend.md
@@ -0,0 +1,261 @@
+# Frontend Development Guidelines
+
+This document describes various guidelines to ensure consistency and quality
+across GitLab's frontend team.
+
+## Overview
+
+GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
+[Hamlit][hamlit]. Be wary of [the limitations that come with using
+Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
+[ES6 by way of Babel][es6].
+
+The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
+minification, and compression of our assets.
+
+[jQuery][jquery] is used throughout the application's JavaScript, with
+[Vue.js][vue] for particularly advanced, dynamic elements.
+
+### Vue
+
+For more complex frontend features, we recommend using Vue.js. It shares
+some ideas with React.js as well as Angular.
+
+To get started with Vue, read through [their documentation][vue-docs].
+
+## Performance
+
+### Resources
+
+- [WebPage Test][web-page-test] for testing site loading time and size.
+- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
+- [Profiling with Chrome DevTools][google-devtools-profiling]
+- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
+
+### Page-specific JavaScript
+
+Certain pages may require the use of a third party library, such as [d3][d3] for
+the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
+libraries increase the page size significantly, and impact load times due to
+bandwidth bottlenecks and the browser needing to parse more JavaScript.
+
+In cases where libraries are only used on a few specific pages, we use
+"page-specific JavaScript" to prevent the main `application.js` file from
+becoming unnecessarily large.
+
+Steps to split page-specific JavaScript from the main `application.js`:
+
+1. Create a directory for the specific page(s), e.g. `graphs/`.
+1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
+1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
+1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
+1. Add the new "bundle" file to the list of precompiled assets in
+`config/application.rb`.
+  - For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
+1. Move code reliant on these libraries into the `graphs` directory.
+1. In the relevant views, add the scripts to the page with the following:
+
+```haml
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_tag('lib/chart.js')
+  = page_specific_javascript_tag('graphs/graphs_bundle.js')
+```
+
+The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
+is separated from the bundle file so it can be cached separately from the bundle
+and reused for other pages that also rely on the library. For an example, see
+[this Haml file][page-specific-js-example].
+
+### Minimizing page size
+
+A smaller page size means the page loads faster (especially important on mobile
+and poor connections), the page is parsed more quickly by the browser, and less
+data is used for users with capped data plans.
+
+General tips:
+
+- Don't add new fonts.
+- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
+- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us).
+- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
+- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
+
+## Accessibility
+
+### Resources
+
+[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
+are useful for testing for potential accessibility problems in GitLab.
+
+Accessibility best-practices and more in-depth information is available on
+[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
+
+## Security
+
+### Resources
+
+[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
+[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
+potential problems and ensuring compliance with security best practices.
+
+<!-- Uncomment these sections when CSP/SRI are implemented.
+### Content Security Policy (CSP)
+
+Content Security Policy is a web standard that intends to mitigate certain
+forms of Cross-Site Scripting (XSS) as well as data injection.
+
+Content Security Policy rules should be taken into consideration when
+implementing new features, especially those that may rely on connection with
+external services.
+
+GitLab's CSP is used for the following:
+
+- Blocking plugins like Flash and Silverlight from running at all on our pages.
+- Blocking the use of scripts and stylesheets downloaded from external sources.
+- Upgrading `http` requests to `https` when possible.
+- Preventing `iframe` elements from loading in most contexts.
+
+Some exceptions include:
+
+- Scripts from Google Analytics and Piwik if either is enabled.
+- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
+- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
+
+We use [the Secure Headers gem][secure_headers] to enable Content
+Security Policy headers in the GitLab Rails app.
+
+Some resources on implementing Content Security Policy:
+
+- [MDN Article on CSP][mdn-csp]
+- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
+- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
+
+### Subresource Integrity (SRI)
+
+Subresource Integrity prevents malicious assets from being provided by a CDN by
+guaranteeing that the asset downloaded is identical to the asset the server
+is expecting.
+
+The Rails app generates a unique hash of the asset, which is used as the
+asset's `integrity` attribute. The browser generates the hash of the asset
+on-load and will reject the asset if the hashes do not match.
+
+All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
+see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
+
+Some resources on implementing Subresource Integrity:
+
+- [MDN Article on SRI][mdn-sri]
+- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
+
+-->
+
+### Including external resources
+
+External fonts, CSS, and JavaScript should never be used with the exception of
+Google Analytics and Piwik - and only when the instance has enabled it. Assets
+should always be hosted and served locally from the GitLab instance. Embedded
+resources via `iframes` should never be used except in certain circumstances
+such as with ReCaptcha, which cannot be used without an `iframe`.
+
+### Avoiding inline scripts and styles
+
+In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
+
+While inline scripts can be useful, they're also a security concern. If
+user-supplied content is unintentionally left un-sanitized, malicious users can
+inject scripts into the web app.
+
+Inline styles should be avoided in almost all cases, they should only be used
+when no alternatives can be found. This allows reusability of styles as well as
+readability.
+
+## Style guides and linting
+
+See the relevant style guides for our guidelines and for information on linting:
+
+- [SCSS][scss-style-guide]
+
+## Testing
+
+Feature tests need to be written for all new features. Regression tests
+also need to be written for all bug fixes to prevent them from occurring
+again in the future.
+
+See [the Testing Standards and Style Guidelines](testing.md) for more
+information.
+
+### Running frontend tests
+
+`rake teaspoon` runs the frontend-only (JavaScript) tests.
+It consists of two subtasks:
+
+- `rake teaspoon:fixtures` (re-)generates fixtures
+- `rake teaspoon:tests` actually executes the tests
+
+As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
+(and saves you some time).
+
+If you need to debug your tests and/or application code while they're
+running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
+in your browser, open DevTools, and run tests for individual files by clicking 
+on them. This is also much faster than setting up and running tests from the 
+command line.
+
+Please note: Not all of the frontend fixtures are generated. Some are still static
+files. These will not be touched by `rake teaspoon:fixtures`.
+
+## Supported browsers
+
+For our currently-supported browsers, see our [requirements][requirements].
+
+[rails]: http://rubyonrails.org/
+[haml]: http://haml.info/
+[hamlit]: https://github.com/k0kubun/hamlit
+[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
+[scss]: http://sass-lang.com/
+[es6]: https://babeljs.io/
+[sprockets]: https://github.com/rails/sprockets
+[jquery]: https://jquery.com/
+[vue]: http://vuejs.org/
+[vue-docs]: http://vuejs.org/guide/index.html
+[web-page-test]: http://www.webpagetest.org/
+[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
+[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
+[browser-diet]: https://browserdiet.com/
+[d3]: https://d3js.org/
+[chartjs]: http://www.chartjs.org/
+[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
+[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[observatory-cli]: https://github.com/mozilla/http-observatory-cli
+[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
+[secure_headers]: https://github.com/twitter/secureheaders
+[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
+[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
+[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
+[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
+[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
+[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
+[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+[github-eng-sri]: http://githubengineering.com/subresource-integrity/
+[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
+[scss-style-guide]: scss_styleguide.md
+[requirements]: ../install/requirements.md#supported-web-browsers
+
+## Gotchas
+
+### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors
+
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
+can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
+have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
+working in (`git mv <file.js> <file.js.es6>`). 
+
+Similar errors will be thrown if you're using 
+any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html)
+whether or not you've updated the file extension.
+
+
+
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 159d5ce286db39ed0fe2151f76adf42646236df6..b25ce79e89f25da4a1b7763cc735288b33869194 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -41,9 +41,9 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
 
 [Exception]: http://stackoverflow.com/q/10048173/223897
 
-## Don't use inline CoffeeScript/JavaScript in views
+## Don't use inline JavaScript in views
 
-Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
+Using the inline `:javascript` Haml filters comes with a
 performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
 
 _**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
@@ -51,9 +51,7 @@ in an initializer._
 
 ### Further reading
 
-- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
 - Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
-- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
 
 ## ID-based CSS selectors need to be a bit more specific
 
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index c2272ab0a2bb844af263a53de2c543fe142677b1..b8669964c84b74858fd6c285b47507ffa04d0a50 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields
 the supplied block while passing `Gitlab::Metrics::Instrumentation` as its
 argument. An example:
 
-```
+```ruby
 Gitlab::Metrics::Instrumentation.configure do |conf|
   conf.instrument_method(Foo, :bar)
   conf.instrument_method(Foo, :baz)
@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer
 
 Instrumenting a single method:
 
-```
+```ruby
 Gitlab::Metrics::Instrumentation.configure do |conf|
   conf.instrument_method(User, :find_by)
 end
@@ -49,7 +49,7 @@ end
 
 Instrumenting an entire class hierarchy:
 
-```
+```ruby
 Gitlab::Metrics::Instrumentation.configure do |conf|
   conf.instrument_class_hierarchy(ActiveRecord::Base)
 end
@@ -57,7 +57,7 @@ end
 
 Instrumenting all public class methods:
 
-```
+```ruby
 Gitlab::Metrics::Instrumentation.configure do |conf|
   conf.instrument_methods(User)
 end
@@ -68,7 +68,7 @@ end
 The easiest way to check if a method has been instrumented is to check its
 source location. For example:
 
-```
+```ruby
 method = Rugged::TagCollection.instance_method(:[])
 
 method.source_location
@@ -137,3 +137,18 @@ end
 ```
 
 Here the final value of `sleep_real_time` will be `3`, _not_ `1`.
+
+## Tracking Custom Events
+
+Besides instrumenting code GitLab Performance Monitoring also supports tracking
+of custom events. This is primarily intended to be used for tracking business
+metrics such as the number of Git pushes, repository imports, and so on.
+
+To track a custom event simply call `Gitlab::Metrics.add_event` passing it an
+event name and a custom set of (optional) tags. For example:
+
+```ruby
+Gitlab::Metrics.add_event(:user_login, email: current_user.email)
+```
+
+Event names should be verbs such as `push_repository` and `remove_branch`.
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 8c8c7486ffff6c263006ea2173a109248e98f0e6..5d177eb26eefe42c2a4f4bc978640e042487472b 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -54,6 +54,7 @@ Libraries with the following licenses are acceptable for use:
 - [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative.
 - [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
 - [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [Creative Commons Zero (CC0)][CC0]: A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
 
 ## Unacceptable Licenses
 
@@ -61,6 +62,7 @@ Libraries with the following licenses are unacceptable for use:
 
 - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
 - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
+- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
 
 ## Notes
 
@@ -85,9 +87,12 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
 [BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
 [BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
 [ISC]: https://opensource.org/licenses/ISC
+[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
 [GPL]: http://choosealicense.com/licenses/gpl-3.0/
 [GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt
 [GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt
 [AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/
 [GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL
 [OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
+[OSL]: https://opensource.org/licenses/OSL-3.0
+[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
new file mode 100644
index 0000000000000000000000000000000000000000..0363bf8c1d54b1f5c3d377e6e38e0b5343ae4dfd
--- /dev/null
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -0,0 +1,171 @@
+# Merge Request Performance Guidelines
+
+To ensure a merge request does not negatively impact performance of GitLab
+_every_ merge request **must** adhere to the guidelines outlined in this
+document. There are no exceptions to this rule unless specifically discussed
+with and agreed upon by merge request endbosses and performance specialists.
+
+To measure the impact of a merge request you can use
+[Sherlock](profiling.md#sherlock). It's also highly recommended that you read
+the following guides:
+
+* [Performance Guidelines](performance.md)
+* [What requires downtime?](what_requires_downtime.md)
+
+## Impact Analysis
+
+**Summary:** think about the impact your merge request may have on performance
+and those maintaining a GitLab setup.
+
+Any change submitted can have an impact not only on the application itself but
+also those maintaining it and those keeping it up and running (e.g. production
+engineers). As a result you should think carefully about the impact of your
+merge request on not only the application but also on the people keeping it up
+and running.
+
+Can the queries used potentially take down any critical services and result in
+engineers being woken up in the night? Can a malicious user abuse the code to
+take down a GitLab instance? Will my changes simply make loading a certain page
+slower? Will execution time grow exponentially given enough load or data in the
+database?
+
+These are all questions one should ask themselves before submitting a merge
+request. It may sometimes be difficult to assess the impact, in which case you
+should ask a performance specialist to review your code. See the "Reviewing"
+section below for more information.
+
+## Performance Review
+
+**Summary:** ask performance specialists to review your code if you're not sure
+about the impact.
+
+Sometimes it's hard to assess the impact of a merge request. In this case you
+should ask one of the merge request (mini) endbosses to review your changes. You
+can find a list of these endbosses at <https://about.gitlab.com/team/>. An
+endboss in turn can request a performance specialist to review the changes.
+
+## Query Counts
+
+**Summary:** a merge request **should not** increase the number of executed SQL
+queries unless absolutely necessary.
+
+The number of queries executed by the code modified or added by a merge request
+must not increase unless absolutely necessary. When building features it's
+entirely possible you will need some extra queries, but you should try to keep
+this at a minimum.
+
+As an example, say you introduce a feature that updates a number of database
+rows with the same value. It may be very tempting (and easy) to write this using
+the following pseudo code:
+
+```ruby
+objects_to_update.each do |object|
+  object.some_field = some_value
+  object.save
+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).
+
+In this particular case the workaround is fairly easy:
+
+```ruby
+objects_to_update.update_all(some_field: some_value)
+```
+
+This uses ActiveRecord's `update_all` method to update all rows in a single
+query. This in turn makes it much harder for this code to overload a database.
+
+## Executing Queries in Loops
+
+**Summary:** SQL queries **must not** be executed in a loop unless absolutely
+necessary.
+
+Executing SQL queries in a loop can result in many queries being executed
+depending on the number of iterations in a loop. This may work fine for a
+development environment with little data, but in a production environment this
+can quickly spiral out of control.
+
+There are some cases where this may be needed. If this is the case this should
+be clearly mentioned in the merge request description.
+
+## Eager Loading
+
+**Summary:** always eager load associations when retrieving more than one row.
+
+When retrieving multiple database records for which you need to use any
+associations you **must** eager load these associations. For example, if you're
+retrieving a list of blog posts and you want to display their authors you
+**must** eager load the author associations.
+
+In other words, instead of this:
+
+```ruby
+Post.all.each do |post|
+  puts post.author.name
+end
+```
+
+You should use this:
+
+```ruby
+Post.all.includes(:author).each do |post|
+  puts post.author.name
+end
+```
+
+## Memory Usage
+
+**Summary:** merge requests **must not** increase memory usage unless absolutely
+necessary.
+
+A merge request must not increase the memory usage of GitLab by more than the
+absolute bare minimum required by the code. This means that if you have to parse
+some large document (e.g. an HTML document) it's best to parse it as a stream
+whenever possible, instead of loading the entire input into memory. Sometimes
+this isn't possible, in that case this should be stated explicitly in the merge
+request.
+
+## Lazy Rendering of UI Elements
+
+**Summary:** only render UI elements when they're actually needed.
+
+Certain UI elements may not always be needed. For example, when hovering over a
+diff line there's a small icon displayed that can be used to create a new
+comment. Instead of always rendering these kind of elements they should only be
+rendered when actually needed. This ensures we don't spend time generating
+Haml/HTML when it's not going to be used.
+
+## Instrumenting New Code
+
+**Summary:** always add instrumentation for new classes, modules, and methods.
+
+Newly added classes, modules, and methods must be instrumented. This ensures
+we can track the performance of this code over time.
+
+For more information see [Instrumentation](instrumentation.md). This guide
+describes how to add instrumentation and where to add it.
+
+## Use of Caching
+
+**Summary:** cache data in memory or in Redis when it's needed multiple times in
+a transaction or has to be kept around for a certain time period.
+
+Sometimes certain bits of data have to be re-used in different places during a
+transaction. In these cases this data should be cached in memory to remove the
+need for running complex operations to fetch the data. You should use Redis if
+data should be cached for a certain time period instead of the duration of the
+transaction.
+
+For example, say you process multiple snippets of text containiner username
+mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the
+user objects for every username we can remove the need for running the same
+query for every mention of `@alice`.
+
+Caching data per transaction can be done using
+[RequestStore](https://github.com/steveklabnik/request_store). Caching data in
+Redis can be done using [Rails' caching
+system](http://guides.rubyonrails.org/caching_with_rails.html).
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index b8fab3aaff7e376d5cf20b3d4aa978a033182e45..fd8335d251e258a9e0d10a347f74eeb65ab0c276 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your
 migrations are written carefully, can be applied online and adhere to the style guide below.
 
 Migrations should not require GitLab installations to be taken offline unless
-_absolutely_ necessary. If a migration requires downtime this should be
-clearly mentioned during the review process as well as being documented in the
-monthly release post. For more information see the "Downtime Tagging" section
-below.
+_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
+page. If a migration requires downtime, this should be clearly mentioned during
+the review process, as well as being documented in the monthly release post. For
+more information, see the "Downtime Tagging" section below.
 
 When writing your migrations, also consider that databases might have stale data
 or inconsistencies and guard for that. Try to make as little assumptions as possible
@@ -60,7 +60,7 @@ migration was tested.
 
 If you need to remove index, please add a condition like in following example:
 
-```
+```ruby
 remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
 ```
 
@@ -75,7 +75,7 @@ need for downtime. To use this method you must disable transactions by calling
 the method `disable_ddl_transaction!` in the body of your migration class like
 so:
 
-```
+```ruby
 class MyMigration < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
   disable_ddl_transaction!
@@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class.
 For example, to add the column `foo` to the `projects` table with a default
 value of `10` you'd write the following:
 
-```
+```ruby
 class MyMigration < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
   disable_ddl_transaction!
@@ -111,6 +111,28 @@ class MyMigration < ActiveRecord::Migration
 end
 ```
 
+
+## Integer column type
+
+By default, an integer column can hold up to a 4-byte (32-bit) number. That is
+a max value of 2,147,483,647. Be aware of this when creating a column that will
+hold file sizes in byte units. If you are tracking file size in bytes this
+restricts the maximum file size to just over 2GB.
+
+To allow an integer column to hold up to an 8-byte (64-bit) number, explicitly
+set the limit to 8-bytes. This will allow the column to hold a value up to
+9,223,372,036,854,775,807.
+
+Rails migration example:
+
+```ruby
+add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+
+# or
+
+add_column(:projects, :foo, :integer, default: 10, limit: 8)
+```
+
 ## Testing
 
 Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
@@ -123,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin
 
 Example with Arel:
 
-```
+```ruby
 users = Arel::Table.new(:users)
 users.group(users[:user_id]).having(users[:id].count.gt(5))
 
@@ -132,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5))
 
 Example with plain SQL and `quote_string` helper:
 
-```
+```ruby
 select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
   tag_name = quote_string(tag["name"])
   duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
index e03adcaadea5d38ca247b9025bcdba0283ee0405..32aac2529a444b6529f3b497d4cdccc4bd1a3d28 100644
--- a/doc/development/newlines_styleguide.md
+++ b/doc/development/newlines_styleguide.md
@@ -2,7 +2,7 @@
 
 This style guide recommends best practices for newlines in Ruby code.
 
-## Rule: separate code with newlines only when it makes sense from logic perspectice
+## Rule: separate code with newlines only to group together related logic
 
 ```ruby
 # bad
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 7ff603e2c4a91aff658cf9acf1c71c052eae692e..8337c2d9cb35b23ce683058518491acbbec2c056 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -34,10 +34,11 @@ graphs/dashboards.
 
 ## Tooling
 
-GitLab provides two built-in tools to aid the process of improving performance:
+GitLab provides built-in tools to aid the process of improving performance:
 
 * [Sherlock](profiling.md#sherlock)
-* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md)
+* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md)
+* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
 
 GitLab employees can use GitLab.com's performance monitoring systems located at
 <http://performance.gitlab.net>, this requires you to log in using your
@@ -253,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of
 referencing an object directly may even slow code down.
 
 [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
-[yorickpeterse]: https://gitlab.com/u/yorickpeterse
+[yorickpeterse]: https://gitlab.com/yorickpeterse
 [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
diff --git a/doc/development/post_deployment_migrations.md b/doc/development/post_deployment_migrations.md
new file mode 100644
index 0000000000000000000000000000000000000000..cfc91539beee3cc2649cd0149e2931439252374f
--- /dev/null
+++ b/doc/development/post_deployment_migrations.md
@@ -0,0 +1,75 @@
+# Post Deployment Migrations
+
+Post deployment migrations are regular Rails migrations that can optionally be
+executed after a deployment. By default these migrations are executed alongside
+the other migrations. To skip these migrations you will have to set the
+environment variable `SKIP_POST_DEPLOYMENT_MIGRATIONS` to a non-empty value
+when running `rake db:migrate`.
+
+For example, this would run all migrations including any post deployment
+migrations:
+
+```bash
+bundle exec rake db:migrate
+```
+
+This however will skip post deployment migrations:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rake db:migrate
+```
+
+## Deployment Integration
+
+Say you're using Chef for deploying new versions of GitLab and you'd like to run
+post deployment migrations after deploying a new version. Let's assume you
+normally use the command `chef-client` to do so. To make use of this feature
+you'd have to run this command as follows:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true sudo chef-client
+```
+
+Once all servers have been updated you can run `chef-client` again on a single
+server _without_ the environment variable.
+
+The process is similar for other deployment techniques: first you would deploy
+with the environment variable set, then you'll essentially re-deploy a single
+server but with the variable _unset_.
+
+## Creating Migrations
+
+To create a post deployment migration you can use the following Rails generator:
+
+```bash
+bundle exec rails g post_deployment_migration migration_name_here
+```
+
+This will generate the migration file in `db/post_migrate`. These migrations
+behave exactly like regular Rails migrations.
+
+## Use Cases
+
+Post deployment migrations can be used to perform migrations that mutate state
+that an existing version of GitLab depends on. For example, say you want to
+remove a column from a table. This requires downtime as a GitLab instance
+depends on this column being present while it's running. Normally you'd follow
+these steps in such a case:
+
+1. Stop the GitLab instance
+2. Run the migration removing the column
+3. Start the GitLab instance again
+
+Using post deployment migrations we can instead follow these steps:
+
+1. Deploy a new version of GitLab while ignoring post deployment migrations
+2. Re-run `rake db:migrate` but without the environment variable set
+
+Here we don't need any downtime as the migration takes place _after_ a new
+version (which doesn't depend on the column anymore) has been deployed.
+
+Some other examples where these migrations are useful:
+
+* Cleaning up data generated due to a bug in GitLab
+* Removing tables
+* Migrating jobs from one Sidekiq queue to another
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index a7175f3f87e708b836c626ddfe5e93214a7be3d2..827db7e99b88cdf6fabeb3b709ad87eeac631773 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -42,14 +42,6 @@ To run several tests inside one directory:
 If you want to use [Spring](https://github.com/rails/spring) set
 `ENABLE_SPRING=1` in your environment.
 
-## Generate searchable docs for source code
-
-You can find results under the `doc/code` directory.
-
-```
-bundle exec rake gitlab:generate_docs
-```
-
 ## Generate API documentation for project services (e.g. Slack)
 
 ```
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 65cdd74bdb6ab9d6980f21a0dc7aa7e3065745f3..73893f9dd464d356fecc54a117679313b4a485f7 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -129,7 +129,7 @@ Various methods for opening and reading files in Ruby can be used to read the
 standard output of a process instead of a file.  The following two commands do
 roughly the same:
 
-```
+```ruby
 `touch /tmp/pawned-by-backticks`
 File.read('|touch /tmp/pawned-by-file-read')
 ```
@@ -142,7 +142,7 @@ attacker cannot control the start of the filename string you are opening.  For
 instance, the following is sufficient to protect against accidentally starting
 a shell command with `|`:
 
-```
+```ruby
 # we assume repo_path is not controlled by the attacker (user)
 path = File.join(repo_path, user_input)
 # path cannot start with '|' now.
@@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user
 access to a certain directory on disk, but the user manages to open a file
 outside that directory by taking advantage of the `../` path notation.
 
-```
+```ruby
 # Suppose the user gave us a path and they are trying to trick us
 user_input = '../other-repo.git/other-file'
 
@@ -177,7 +177,7 @@ File.open(full_path) do # Oops!
 A good way to protect against this is to compare the full path with its
 'absolute path' according to Ruby's `File.absolute_path`.
 
-```
+```ruby
 full_path = File.join(repo_path, user_input)
 if full_path != File.absolute_path(full_path)
   raise "Invalid path: #{full_path.inspect}"
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
new file mode 100644
index 0000000000000000000000000000000000000000..e3a20f29a094cea7034297615b14a4f6880b9137
--- /dev/null
+++ b/doc/development/sidekiq_style_guide.md
@@ -0,0 +1,38 @@
+# Sidekiq Style Guide
+
+This document outlines various guidelines that should be followed when adding or
+modifying Sidekiq workers.
+
+## Default Queue
+
+Use of the "default" queue is not allowed. Every worker should use a queue that
+matches the worker's purpose the closest. For example, workers that are to be
+executed periodically should use the "cronjob" queue.
+
+A list of all available queues can be found in `config/sidekiq_queues.yml`.
+
+## Dedicated Queues
+
+Most workers should use their own queue. To ease this process a worker can
+include the `DedicatedSidekiqQueue` concern as follows:
+
+```ruby
+class ProcessSomethingWorker
+  include Sidekiq::Worker
+  include DedicatedSidekiqQueue
+end
+```
+
+This will set the queue name based on the class' name, minus the `Worker`
+suffix. In the above example this would lead to the queue being
+`process_something`.
+
+In some cases multiple workers do use the same queue. For example, the various
+workers for updating CI pipelines all use the `pipeline` queue. Adding workers
+to existing queues should be done with care, as adding more workers can lead to
+slow jobs blocking work (even for different jobs) on the shared queue.
+
+## Tests
+
+Each Sidekiq worker must be tested using RSpec, just like any other class. These
+tests should be placed in `spec/workers`.
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 513457d203a4875d75d6618c48391a9765ea5290..b0b26ccf57adbb82ec0977d31cc115ebc49e87f4 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -36,8 +36,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at
 `http://localhost:3000/teaspoon` when the Rails server is running.
 
 - JavaScript tests live in `spec/javascripts/`, matching the folder structure of
-  `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding
-  `spec/javascripts/behaviors/autosize_spec.js.coffee` file.
+  `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
+  `spec/javascripts/behaviors/autosize_spec.js.es6` 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.
@@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
 no more than one new `step` definition. If more than that is required, the
 test should be re-implemented using RSpec instead.
 
+## Testing Rake Tasks
+
+To make testing Rake tasks a little easier, there is a helper that can be included
+in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
+`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
+a few other things to make testing Rake tasks easier.
+
+At a minimum, requiring the Rake helper will redirect `stdout`, include the
+runtime task helpers, and include the `RakeHelpers` Spec support module.
+
+The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
+executing tasks simple. See `spec/support/rake_helpers.rb` for all available
+methods.
+
+Example:
+
+```ruby
+require 'rake_helper'
+
+describe 'gitlab:shell rake tasks' do
+  before do
+    Rake.application.rake_require 'tasks/gitlab/shell'
+
+    stub_warn_user_is_not_gitlab
+  end
+
+ describe 'install task' do
+    it 'invokes create_hooks task' do
+      expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+
+      run_rake_task('gitlab:shell:install')
+    end
+  end
+end
+```
+
 ---
 
 [Return to Development documentation](README.md)
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 2574c2c04727c58a76bca7a86d1a04ef2ceff035..bbcd26477f34ff46cdeacd66c73c3ec1569d20f5 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -66,6 +66,12 @@ producing errors whenever it tries to use the `dummy` column.
 As a result of the above downtime _is_ required when removing a column, even
 when using PostgreSQL.
 
+## Renaming Columns
+
+Renaming columns requires downtime as running GitLab instances will continue
+using the old column name until a new version is deployed. This can result
+in the instance producing errors, which in turn can impact the user experience.
+
 ## Changing Column Constraints
 
 Generally changing column constraints requires checking all rows in the table to
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3aa83975ace88d00953bc3445ea2c0257a8ea0e6..d7e3aa35bddcbd8756d9e4f44e1c75c529321e29 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,14 +2,14 @@
 
 Step-by-step guides on the basics of working with Git and GitLab.
 
+- [Command line basics](command-line-commands.md)
 - [Start using Git on the command line](start-using-git.md)
 - [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Command Line basics](command-line-commands.md)
 - [Create a project](create-project.md)
 - [Create a group](create-group.md)
 - [Create a branch](create-branch.md)
 - [Fork a project](fork-project.md)
 - [Add a file](add-file.md)
 - [Add an image](add-image.md)
-- [Create a Merge Request](add-merge-request.md)
-- [Create an Issue](create-issue.md)
+- [Create an issue](create-issue.md)
+- [Create a merge request](add-merge-request.md)
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index 57136ac5c392f3f54f493ebc77a21d052b766d99..e9fbcbc23a9dbc3d853f3b79020efd3384da906e 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -1,31 +1,5 @@
 # How to add a file
 
-You can create a file in your [shell](command-line-commands.md) or in GitLab.
-
-To create a file in GitLab, sign in to GitLab.
-
-Select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-It's a good idea to [create a branch](create-branch.md), but it's not necessary.
-
-Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory:
-
-![Create a file](basicsimages/create_file.png)
-
-Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use :
-
-![File name](basicsimages/file_name.png)
-
-Add all the information that you'd like to include in your file:
-
-![Add information](basicsimages/white_space.png)
-
-Add a commit message based on what you just added and then click on "commit changes":
-
-![Commit changes](basicsimages/commit_changes.png)
-
-### Note
-Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling
-what the directory is about. It's the first document you'll find when you open a directory.
+You can create a file in your [terminal](command-line-commands.md) and push
+to GitLab or you can use the
+[web interface](../user/project/repository/web_editor.md#create-a-file).
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index 236b4248ea2372ba172577b16cef684219a13d41..bf01fe51dc3d1b0f3a7399d6328ff6aa8f995c0b 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -1,42 +1,33 @@
 # How to create a merge request
 
-Merge Requests are useful to integrate separate changes that you've made to a project, on different branches.
+Merge requests are useful to integrate separate changes that you've made to a
+project, on different branches. This is a brief guide on how to create a merge
+request. For more information, check the
+[merge requests documentation](../user/project/merge_requests.md).
 
-To create a new Merge Request, sign in to GitLab.
+---
 
-Go to the project where you'd like to merge your changes:
+1. Before you start, you should have already [created a branch](create-branch.md)
+   and [pushed your changes](basic-git-commands.md) to GitLab.
 
-![Select a project](basicsimages/select_project.png)
+1. You can then go to the project where you'd like to merge your changes and
+   click on the **Merge requests** tab.
 
-Click on "Merge Requests" on the left side of your screen:
+    ![Merge requests](img/project_navbar.png)
 
-![Merge requests](basicsimages/merge_requests.png)
+1. Click on **New merge request** on the right side of the screen.
 
-Click on "+ new Merge Request" on the right side of the screen:
+    ![New Merge Request](img/merge_request_new.png)
 
-![New Merge Request](basicsimages/new_merge_request.png)
+1. Select a source branch and click on the **Compare branches and continue** button.
 
-Select a source branch or branch:
+    ![Select a branch](img/merge_request_select_branch.png)
 
-![Select a branch](basicsimages/select_branch.png)
+1. At a minimum, add a title and a description to your merge request. Optionally,
+   select a user to review your merge request and to accept or close it. You may
+   also select a milestone and labels.
 
-Click on the "compare branches" button:
+    ![New merge request page](img/merge_request_page.png)
 
-![Compare branches](basicsimages/compare_branches.png)
-
-Add a title and a description to your Merge Request:
-
-![Add a title and description](basicsimages/title_description_mr.png)
-
-Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button:
-
-![Add a new merge request](basicsimages/add_new_merge_request.png)
-
-Your Merge Request will be ready to be approved and published.
-
-### Note
-
-After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen.
-You may automatically create a Merge Request from your recently created branch when clicking on this button:
-
-![Automatic MR button](basicsimages/button-create-mr.png)
+1. When ready, click on the **Submit merge request** button. Your merge request
+   will be ready to be approved and published.
diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
deleted file mode 100644
index e60992c4c6a21206ba39889c536aac597dff4361..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/add_new_merge_request.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png
deleted file mode 100644
index 89c860186292fc02e160262d01c49d0af5ede952..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/add_sshkey.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png
deleted file mode 100644
index 2264f3c5bf2460a41c4e676717ed8b596e34eabe..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/branch_info.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png
deleted file mode 100644
index 75fe8313611e209fcbd3cdb1e093cc0adb258590..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/branch_name.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png
deleted file mode 100644
index 8621bc05776006f14c1d040f3bc960dda1fe4b84..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/branches.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png
deleted file mode 100644
index b52ab148839dba8c311790657a0b9d12734743cd..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/button-create-mr.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png
deleted file mode 100644
index 6450deec6fc17e0e5a10c2660eb28990d3b09ac2..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/click-on-new-group.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png
deleted file mode 100644
index a88809c5a3f7da41c4583fe5824f94e5f3ae14f2..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/commit_changes.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png
deleted file mode 100644
index 4abe4517f98f426507c7300a67dcd88b50c97279..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/commit_message.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png
deleted file mode 100644
index 2bfcaf75f016423bdd28c73a4a643a11774a8267..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/commits.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/compare_branches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
deleted file mode 100644
index 8a18453dd05b7989d178960ec1a0a66a07598d57..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/compare_branches.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png
deleted file mode 100644
index 5ebe1b227dd56e027d17f4a408e5c2b917550404..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/create_file.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png
deleted file mode 100644
index 7ecc3baa9900c069663ba8fa8fbce6c60c86af50..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/create_group.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png
deleted file mode 100644
index 9d3e817d0363e7fe476c67d312b6abe8db4f193a..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/edit_file.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png
deleted file mode 100644
index e357cb5c6ab94e36522b8d554c518b75e71b8600..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/file_located.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png
deleted file mode 100644
index 01639c77d0da799cf27cd721b13149de8bdc2b31..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/file_name.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png
deleted file mode 100644
index 6f26d26ae182c2373763182555f0e11615d2d983..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/find_file.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png
deleted file mode 100644
index 1211510aae9b0e927a7476401a4c90071c36fa34..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/find_group.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png
deleted file mode 100644
index 13ff834561627dacdc3b12b08fae83dbe65b2ee5..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/fork.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png
deleted file mode 100644
index 2507d6c295b72ffe8a41aa72e68dce014e4d76e2..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/group_info.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png
deleted file mode 100644
index ef3dca60cc8f08d681014a1b00ce43e0ed3c27e6..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/groups.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png
deleted file mode 100644
index e74dbc13f9ad4548543db00d801d8baaac097eee..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/https.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png
deleted file mode 100644
index 7f304b8e1f29fcd64ebbadf7a8f1b8f99f3c4349..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/image_file.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png
deleted file mode 100644
index 60a6f7973be56c4767b30dd19a2695e528f21ffa..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/issue_title.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png
deleted file mode 100644
index 14e9cdb64e15edc485fbed53f33a2e8ca22e69c5..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/issues.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png
deleted file mode 100644
index 04400173ce8246804328e90c5a315563cb6ebe2c..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/key.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png
deleted file mode 100644
index 570164df18b9d6112d9ae37318f1b5f44770a8c4..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/merge_requests.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/new_issue.png b/doc/gitlab-basics/basicsimages/new_issue.png
deleted file mode 100644
index 94e7503dd8b4784ed4d28e99e2d279f6a1eebb35..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/new_issue.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png
deleted file mode 100644
index 842f5ebed74ca85a7f55b06f376b7afd79f6d259..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/new_merge_request.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png
deleted file mode 100644
index 421e8bc247be7f2ecad18b94918623807e7e5469..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/new_project.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png
deleted file mode 100644
index d5fcf33c4ea86b0483414c76ef07cc094118208d..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/newbranch.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png
deleted file mode 100644
index 578ebee4440f26167a9dc04a50c1b689114e2ed2..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/paste_sshkey.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png
deleted file mode 100644
index cb3f79f1879b185e2ab55edb5a6ded59115678ac..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/profile_settings.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png
deleted file mode 100644
index e1adb8d48c21c125838f23589715d01c5c2dc6c4..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/project_info.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png
deleted file mode 100644
index 33b978dd89902255f3e3ee34a1ae4b0e360c9b9c..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/select-group.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png
deleted file mode 100644
index aee22c638db37355f418ffbb205bb85c1ca6547f..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/select-group2.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png
deleted file mode 100644
index f72a3ffb57fc005d4960f685dd94f78b406fcd67..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/select_branch.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png
deleted file mode 100644
index 3bb832ea8d001d49adf400264db2ec8cc9973a2e..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/select_project.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png
deleted file mode 100644
index 78637013d9b2dc05c9c2f7ba06722387a1c101e4..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/settings.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png
deleted file mode 100644
index c87f11a9d3daa2d8b155797bb11b4372bb700130..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/shh_keys.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png
deleted file mode 100644
index 78b854c8903ad855ad626b9aa9f0acb5a6149a3c..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/submit_new_issue.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png
deleted file mode 100644
index c31d61ec3366800147c3934d854f065165aeedee..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/title_description_mr.png and /dev/null differ
diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png
deleted file mode 100644
index eaa969bdcf4cdc1fe4ceafb28c62cde36bfac28c..0000000000000000000000000000000000000000
Binary files a/doc/gitlab-basics/basicsimages/white_space.png and /dev/null differ
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index addd3b6b6eb6bb60ee17494baeee151286e20da4..3b075ff5fc0aad1bf6252c9c463cda2fb301772c 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -4,18 +4,21 @@
 
 In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab.
 
-When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen.
+When you are on your Dashboard, click on the project that you'd like to clone.
+To work in the project, you can copy a link to the Git repository through a SSH
+or a HTTPS protocol. SSH is easier to use after it's been
+[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select
+HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard'
+button (you'll have to paste it on your shell in the next step).
 
-![Select a project](basicsimages/select_project.png)
-
-To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step).
-
-![Copy the HTTPS or SSH](basicsimages/https.png)
+![Copy the HTTPS or SSH](img/project_clone_url.png)
 
 ## On the command line
 
 ### Clone your project
+
 Go to your computer's shell and type the following command:
+
 ```
 git clone PASTE HTTPS OR SSH HERE
 ```
@@ -23,26 +26,31 @@ git clone PASTE HTTPS OR SSH HERE
 A clone of the project will be created in your computer.
 
 ### Go into a project, directory or file to work in it
+
 ```
 cd NAME-OF-PROJECT-OR-FILE
 ```
 
 ### Go back one directory or file
+
 ```
 cd ../
 ```
 
 ### View what’s in the directory that you are in
+
 ```
 ls
 ```
 
 ### Create a directory
+
 ```
 mkdir NAME-OF-YOUR-DIRECTORY
 ```
 
 ### Create a README.md or file in directory
+
 ```
 touch README.md
 nano README.md
@@ -53,27 +61,33 @@ nano README.md
 ```
 
 ### Remove a file
+
 ```
 rm NAME-OF-FILE
 ```
 
 ### Remove a directory and all of its contents
+
 ```
 rm -rf NAME-OF-DIRECTORY
 ```
 
 ### View history in the command line
+
 ```
 history
 ```
 
 ### Carry out commands for which the account you are using lacks authority
+
 You will be asked for an administrator’s password.
+
 ```
 sudo
 ```
 
 ### Tell where you are
+
 ```
 pwd
 ```
diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md
index 7556b0f663ebb14b6ae409e1c0100de35b9ddfd2..ad94f0dad292675ecbb6b24b22144c084476648a 100644
--- a/doc/gitlab-basics/create-branch.md
+++ b/doc/gitlab-basics/create-branch.md
@@ -2,38 +2,11 @@
 
 A branch is an independent line of development.
 
-New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project.
-
-To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab.
-
-To create a new branch in GitLab, sign in and then select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-Click on "commits" on the menu on the left side of your screen:
-
-![Commits](basicsimages/commits.png)
-
-Click on the "branches" tab:
-
-![Branches](basicsimages/branches.png)
-
-Click on the "new branch" button on the right side of the screen:
-
-![New branch](basicsimages/newbranch.png)
-
-Fill out the information required:
-
-1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores)
-
-1. On the "create from" space, add the the name of the branch you want to branch off from
-
-1. Click on the button "create branch"
-
-![Branch info](basicsimages/branch_info.png)
-
-### Note:
-
-You will be able to find and select the name of your branch in the white box next to a project's name:
-
-![Branch name](basicsimages/branch_name.png)
+New commits are recorded in the history for the current branch, which results
+in taking the source from someone’s repository (the place where the history of
+your work is stored) at certain point in time, and apply your own changes to it
+in the history of the project.
+
+To add changes to your GitLab project, you should create a branch. You can do
+it in your [terminal](basic-git-commands.md) or by
+[using the web interface](../user/project/repository/web_editor.md#create-a-new-branch).
diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md
index f80ae62e442ed172016d172eabb26831ce7f3ff5..64274ccd5eb21af728576976c93033a7adef1bf9 100644
--- a/doc/gitlab-basics/create-group.md
+++ b/doc/gitlab-basics/create-group.md
@@ -1,43 +1,48 @@
 # How to create a group in GitLab
 
-## Create a group
-
 Your projects in GitLab can be organized in 2 different ways:
-under your own namespace for single projects, such as ´your-name/project-1'; or under groups.
-If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects.
-
-To create a group, follow the instructions below:
+under your own namespace for single projects, such as `your-name/project-1` or
+under groups.
 
-Sign in to [GitLab.com](https://gitlab.com).
+If you organize your projects under a group, it works like a folder. You can
+manage your group members' permissions and access to the projects.
 
-When you are on your Dashboard, click on "Groups" on the left menu of your screen:
+---
 
-![Go to groups](basicsimages/select-group2.png)
+To create a group:
 
-Click on "New group" on the top right side of your screen:
+1. Expand the left sidebar by clicking the three bars at the upper left corner
+   and then navigate to **Groups**.
 
-![New group](basicsimages/click-on-new-group.png)
+    ![Go to groups](img/create_new_group_sidebar.png)
 
-Fill out the information required:
+1. Once in your groups dashboard, click on **New group**.
 
-1. Add a group path or group name (you can't add spaces, so you can use hyphens or underscores)
+    ![Create new group information](img/create_new_group_info.png)
 
-1. Add details or a group description
+1. Fill out the needed information:
 
-1. You can choose a group avatar if you'd like
+    1. Set the "Group path" which will be the namespace under which your projects
+       will be hosted (path can contain only letters, digits, underscores, dashes
+       and dots; it cannot start with dashes or end in dot).
+    1. Optionally, you can add a description so that others can briefly understand
+       what this group is about.
+    1. Optionally, choose and avatar for your project.
+    1. Choose the [visibility level](../public_access/public_access.md).
 
-1. Click on "create group"
+1. Finally, click the **Create group** button.
 
-![Group information](basicsimages/group_info.png)
-
-## Add a project to a group
+## Add a new project to a group
 
 There are 2 different ways to add a new project to a group:
 
-* Select a group and then click on "New project" on the right side of your screen. Then you can [create a project](create-project.md)
+- Select a group and then click on the **New project** button.
+
+    ![New project](img/create_new_project_from_group.png)
 
-![New project](basicsimages/new_project.png)
+    You can then continue on [creating a project](create-project.md).
 
-* When you are [creating a project](create-project.md), click on "create a group" on the bottom right side of your screen
+- While you are [creating a project](create-project.md), select a group namespace
+  you've already created from the dropdown menu.
 
-![Create a group](basicsimages/create_group.png)
+    ![Select group](img/select_group_dropdown.png)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 5221d85b6610274dd129285b5ae29c0ce57a2e9a..13e5a738c894f3188d0dd19f2d09c2506a903fd1 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,27 +1,30 @@
 # How to create an Issue in GitLab
 
-The Issue Tracker is a good place to add things that need to be improved or solved in a project.  
+The issue tracker is a good place to add things that need to be improved or
+solved in a project.
 
-To create an Issue, sign in to GitLab.
+---
 
-Go to the project where you'd like to create the Issue:
+1. Go to the project where you'd like to create the issue and navigate to the
+   **Issues** tab on top.
 
-![Select a project](basicsimages/select_project.png)
+    ![Issues](img/project_navbar.png)
 
-Click on "Issues" on the left side of your screen:
+1. Click on the **New issue** button on the right side of your screen.
 
-![Issues](basicsimages/issues.png)
+    ![New issue](img/new_issue_button.png)
 
-Click on the "+ new issue" button on the right side of your screen:
+1. At the very minimum, add a title and a description to your issue.
+   You may assign it to a user, add a milestone or add labels (all optional).
 
-![New issue](basicsimages/new_issue.png)
+    ![Issue title and description](img/new_issue_page.png)
 
-Add a title and a description to your issue:
+1. When ready, click on **Submit issue**.
 
-![Issue title and description](basicsimages/issue_title.png)
+---
 
-You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
-
-![Submit new issue](basicsimages/submit_new_issue.png)
-
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html).
+Your Issue will now be added to the issue tracker of the project you opened it
+at and will be ready to be reviewed. You can comment on it and mention the
+people involved. You can also link issues to the merge requests where the issues
+are solved. To do this, you can use an
+[issue closing pattern](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index f737dffc0248ae9af553305fddc85bfc80785b9a..3f45a631b3a8a13211b0435d9482e91f56bd2515 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,21 +1,24 @@
 # How to create a project in GitLab
 
-To create a new project, sign in to GitLab.
+There are two ways to create a new project in GitLab.
 
-Go to your Dashboard and click on "new project" on the right side of your screen.
+1. While in your dashboard, you can create a new project using the **New project**
+   green button or you can use the cross icon in the upper right corner next to
+   your avatar which is always visible.
 
-![Create a project](basicsimages/new_project.png)
+    ![Create a project](img/create_new_project_button.png)
 
-Fill out the required information:
+1. From there you can see several options.
 
-1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores)
+    ![Project information](img/create_new_project_info.png)
 
-1. Your project's description
+1. Fill out the information:
 
-1. Select a [visibility level](https://gitlab.com/help/public_access/public_access)
+  1. "Project name" is the name of your project (you can't use spaces, but you
+     can use hyphens or underscores).
+  1. The "Project description" is optional and will be shown in your project's
+     dashboard so others can briefly understand what your project is about.
+  1. Select a [visibility level](../public_access/public_access.md).
+  1. You can also [import your existing projects](../workflow/importing/README.md).
 
-1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html)
-
-1. Click on "create project"
-
-!![Project information](basicsimages/project_info.png)
+1. Finally, click **Create project**.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index f31c353f2cfffb7a7dbcade77a552f42136ce3bc..b6ebe374de31bc1bea11a412b0cab027f0fdf11e 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,33 +1,37 @@
 # How to create your SSH Keys
 
-You need to connect your computer to your GitLab account through SSH Keys. They are unique for every computer that you link your GitLab account with.
+1. The first thing you need to do is go to your [command line](start-using-git.md)
+   and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
 
-## Generate your SSH Key
+1. Once you do that, login to GitLab with your credentials.
+1. On the upper right corner, click on your avatar and go to your **Profile settings**.
 
-Create an account on GitLab. Sign up and check your email for your confirmation link.
+    ![Profile settings dropdown](img/profile_settings.png)
 
-After you confirm, go to GitLab and sign in to your account.
+1. Navigate to the **SSH keys** tab.
 
-## Add your SSH Key
+    ![SSH Keys](img/profile_settings_ssh_keys.png)
 
-On the left side menu, click on "profile settings" and then click on "SSH Keys":
+3. Paste your **public** key that you generated in the first step in the 'Key'
+   box.
 
-![SSH Keys](basicsimages/shh_keys.png)
+    ![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
 
-Then click on the green button "Add SSH Key":
+1. Optionally, give it a descriptive title so that you can recognize it in the
+   event you add multiple keys.
 
-![Add SSH Key](basicsimages/add_sshkey.png)
+    ![SSH key title](img/profile_settings_ssh_keys_title.png)
 
-There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it:
+1. Finally, click on **Add key** to add it to GitLab. You will be able to see
+   its fingerprint, its title and creation date.
 
-![Paste SSH Key](basicsimages/paste_sshkey.png)
+    ![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
 
-## To generate an SSH Key on your command line
 
-Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it.
+>**Note:**
+Once you add a key, you cannot edit it, only remove it. In case the paste
+didn't work, you will have to remove the offending key and re-add it.
 
-Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically.
+---
 
-![Paste SSH Key](basicsimages/key.png)
-
-Now, you'll be able to use Git over SSH, instead of Git over HTTP.
+Congratulations! You are now ready to use Git over SSH, instead of Git over HTTP!
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 5f8b81ea91924a3b86109948b63a0f27598e3805..6c232fe6086243cc143a501b92a5802d80807463 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -1,19 +1,20 @@
 # How to fork a project
 
-A fork is a copy of an original repository that you can put somewhere else
-or where you can experiment and apply changes that you can later decide if
+A fork is a copy of an original repository that you can put in another namespace
+where you can experiment and apply changes that you can later decide if
 publishing or not, without affecting your original project.
 
 It takes just a few steps to fork a project in GitLab.
 
-Sign in to GitLab.
+1. Go to a project's dashboard under the **Project** tab and click on the
+   **Fork** button.
 
-Select a project on the right side of your screen:
+    ![Click on Fork button](img/fork_new.png)
 
-![Select a project](basicsimages/select_project.png)
+1. You will be asked where to fork the repository. Click on the user or group
+   to where you'd like to add the forked project.
 
-Click on the "fork" button on the right side of your screen:
+    ![Choose namespace](img/fork_choose_namespace.png)
 
-![Fork](basicsimages/fork.png)
-
-Click on the user or group to where you'd like to add the forked project.
+1. After a few moments, depending on the repository's size, the forking will
+   complete.
diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8eddfd1bbb10cf5d23d1c30da3cd5a3335f8c37
Binary files /dev/null and b/doc/gitlab-basics/img/create_new_group_info.png differ
diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png
new file mode 100644
index 0000000000000000000000000000000000000000..28017ee02e0018ff178688fd5519afec41006719
Binary files /dev/null and b/doc/gitlab-basics/img/create_new_group_sidebar.png differ
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..e7c794d943fb975e82e5f842852f3e36f562c0df
Binary files /dev/null and b/doc/gitlab-basics/img/create_new_project_button.png differ
diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d41d17f9cad3b8aedd31dc7c5a21f71945513fc
Binary files /dev/null and b/doc/gitlab-basics/img/create_new_project_from_group.png differ
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
new file mode 100644
index 0000000000000000000000000000000000000000..16d56f0707f7cbe3904b2b4991a2c0fa5cce4cd4
Binary files /dev/null and b/doc/gitlab-basics/img/create_new_project_info.png differ
diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png
new file mode 100644
index 0000000000000000000000000000000000000000..82c9c3bd39e31d530ae25371edf246a5c015f548
Binary files /dev/null and b/doc/gitlab-basics/img/fork_choose_namespace.png differ
diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png
new file mode 100644
index 0000000000000000000000000000000000000000..41885223286f6fff5702094aa445fa16cb482ff3
Binary files /dev/null and b/doc/gitlab-basics/img/fork_new.png differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
new file mode 100644
index 0000000000000000000000000000000000000000..0aba5743f0102ffe6b1d38b2c58463f1082b1ca3
Binary files /dev/null and b/doc/gitlab-basics/img/merge_request_new.png differ
diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..68c3bbf94444b5586f3dfdc8ac04abbd94b4a18c
Binary files /dev/null and b/doc/gitlab-basics/img/merge_request_page.png differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
new file mode 100644
index 0000000000000000000000000000000000000000..516436ff6cc08098a9ba6f779eba562d97b400e4
Binary files /dev/null and b/doc/gitlab-basics/img/merge_request_select_branch.png differ
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..46b626bed653c6cd13c7b38e59d32c9db22b9fa2
Binary files /dev/null and b/doc/gitlab-basics/img/new_issue_button.png differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..843504130b77ebac79dd36b1b282cc5fce1a90e4
Binary files /dev/null and b/doc/gitlab-basics/img/new_issue_page.png differ
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..f0abd47884934ec57b1c76fe21f5cbd9db36fc93
Binary files /dev/null and b/doc/gitlab-basics/img/profile_settings.png differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c9a42fe10c49e9152842d8eacce0bb23856cf35
Binary files /dev/null and b/doc/gitlab-basics/img/profile_settings_ssh_keys.png differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd7add6937fb7995359f6faa15c1a92d56450a53
Binary files /dev/null and b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
new file mode 100644
index 0000000000000000000000000000000000000000..095beb02be8fdd8d1095baa164e17da802986f3c
Binary files /dev/null and b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b998a7f948f3eee2225fef7f2a0bf30d9cc6964
Binary files /dev/null and b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png differ
diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png
new file mode 100644
index 0000000000000000000000000000000000000000..eed430e103698719175717e9315eaea545502370
Binary files /dev/null and b/doc/gitlab-basics/img/project_clone_url.png differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
new file mode 100644
index 0000000000000000000000000000000000000000..97cf3cd9702dce97b9bb0b98b508625e684ad35c
Binary files /dev/null and b/doc/gitlab-basics/img/project_navbar.png differ
diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png
similarity index 100%
rename from doc/gitlab-basics/basicsimages/public_file_link.png
rename to doc/gitlab-basics/img/public_file_link.png
diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d8b89c2df905970167665a78b0ade40eac1bff2
Binary files /dev/null and b/doc/gitlab-basics/img/select_group_dropdown.png differ
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index b61f436c1a4f254c35400ae23754625770e081fb..42cd8bb3e485702de4de0cf35879b0bb7b398202 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,11 +1,10 @@
 # Start using Git on the command line
 
-If you want to start using a Git and GitLab, make sure that you have created an
-account on GitLab.
+If you want to start using Git and GitLab, make sure that you have created and/or signed into an account on GitLab.
 
 ## Open a shell
 
-Depending on your operating system, find the shell of your preference. Here are some suggestions.
+Depending on your operating system, you will need to use a shell of your preference. Here are some suggestions:
 
 - [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on  Mac OSX
 
@@ -22,19 +21,19 @@ Type the following command and then press enter:
 git --version
 ```
 
-You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
 
 If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window.
 
-After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
+After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
 
 ## Add your Git username and set your email
 
-It is important because every Git commit that you create will use this information.
+It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author.
 
 On your shell, type the following command to add your username:
 ```
-git config --global user.name ADD YOUR USERNAME
+git config --global user.name "YOUR_USERNAME"
 ```
 
 Then verify that you have the correct username:
@@ -44,7 +43,7 @@ git config --global user.name
 
 To set your email address, type the following command:
 ```
-git config --global user.email ADD YOUR EMAIL
+git config --global user.email "your_email_address@example.com"
 ```
 
 To verify that you entered your email correctly, type:
@@ -52,7 +51,7 @@ To verify that you entered your email correctly, type:
 git config --global user.email
 ```
 
-You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project.
+You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project.
 
 ## Check your information
 
@@ -76,7 +75,7 @@ git pull REMOTE NAME-OF-BRANCH -u
 (REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
 
 ### Create a branch
-Spaces won't be recognized, so you need to use a hyphen or underscore.
+Spaces won't be recognized, so you will need to use a hyphen or underscore.
 ```
 git checkout -b NAME-OF-BRANCH
 ```
@@ -127,4 +126,3 @@ You need to be in the master branch.
 git checkout master
 git merge NAME-OF-BRANCH
 ```
-
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 5a9a158287741814e3ec1450290e2a5b23110570..db0f03f2c98bb45af3e52d18175db46e5528403f 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1,302 +1 @@
-# Reply by email
-
-GitLab can be set up to allow users to comment on issues and merge requests by
-replying to notification emails.
-
-## Requirement
-
-Reply by email requires an IMAP-enabled email account. GitLab allows you to use
-three strategies for this feature:
-- using email sub-addressing
-- using a dedicated email address
-- using a catch-all mailbox
-
-### Email sub-addressing
-
-**If your provider or server supports email sub-addressing, we recommend using it.**
-
-[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
-in the mailbox for `user@example.com`, and is supported by providers such as
-Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
-mail server which you can run on-premises.
-
-### Dedicated email address
-
-This solution is really simple to set up: you just have to create an email
-address dedicated to receive your users' replies to GitLab notifications.
-
-### Catch-all mailbox
-
-A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
-"catch all" the emails addressed to the domain that do not exist in the mail
-server.
-
-## How it works?
-
-### 1. GitLab sends a notification email
-
-When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
-header is set to the address defined in your GitLab configuration, with the
-`%{key}` placeholder (if present) replaced by a specific "reply key". In
-addition, this "reply key" is also added to the `References` header.
-
-### 2. You reply to the notification email
-
-When you reply to the notification email, your email client will:
-
-- send the email to the `Reply-To` address it got from the notification email
-- set the `In-Reply-To` header to the value of the `Message-ID` header from the
-  notification email
-- set the `References` header to the value of the `Message-ID` plus the value of
-  the notification email's `References` header.
-
-### 3. GitLab receives your reply to the notification email
-
-When GitLab receives your reply, it will look for the "reply key" in the
-following headers, in this order:
-
-1. the `To` header
-1. the `References` header
-
-If it finds a reply key, it will be able to leave your reply as a comment on
-the entity the notification was about (issue, merge request, commit...).
-
-For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
-please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
-
-## Set it up
-
-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).
-
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
-[these instructions](./postfix.md).
-
-### Omnibus package installations
-
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
-  feature and fill in the details for your specific IMAP server and email account:
-
-    ```ruby
-    # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
-    gitlab_rails['incoming_email_enabled'] = true
-
-    # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
-    # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
-    gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
-    # Email account username
-    # With third party providers, this is usually the full email address.
-    # With self-hosted email servers, this is usually the user part of the email address.
-    gitlab_rails['incoming_email_email'] = "incoming"
-    # Email account password
-    gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
-    # IMAP server host
-    gitlab_rails['incoming_email_host'] = "gitlab.example.com"
-    # IMAP server port
-    gitlab_rails['incoming_email_port'] = 143
-    # Whether the IMAP server uses SSL
-    gitlab_rails['incoming_email_ssl'] = false
-    # Whether the IMAP server uses StartTLS
-    gitlab_rails['incoming_email_start_tls'] = false
-
-    # The mailbox where incoming mail will end up. Usually "inbox".
-    gitlab_rails['incoming_email_mailbox_name'] = "inbox"
-    ```
-
-    ```ruby
-    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
-    gitlab_rails['incoming_email_enabled'] = true
-
-    # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
-    # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
-    gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
-    # Email account username
-    # With third party providers, this is usually the full email address.
-    # With self-hosted email servers, this is usually the user part of the email address.
-    gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
-    # Email account password
-    gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
-    # IMAP server host
-    gitlab_rails['incoming_email_host'] = "imap.gmail.com"
-    # IMAP server port
-    gitlab_rails['incoming_email_port'] = 993
-    # Whether the IMAP server uses SSL
-    gitlab_rails['incoming_email_ssl'] = true
-    # Whether the IMAP server uses StartTLS
-    gitlab_rails['incoming_email_start_tls'] = false
-
-    # The mailbox where incoming mail will end up. Usually "inbox".
-    gitlab_rails['incoming_email_mailbox_name'] = "inbox"
-    ```
-
-1. Reconfigure GitLab and restart mailroom for the changes to take effect:
-
-    ```sh
-    sudo gitlab-ctl reconfigure
-    sudo gitlab-ctl restart mailroom
-    ```
-
-1. Verify that everything is configured correctly:
-
-    ```sh
-    sudo gitlab-rake gitlab:incoming_email:check
-    ```
-
-1. Reply by email should now be working.
-
-### Installations from source
-
-1. Go to the GitLab installation directory:
-
-    ```sh
-    cd /home/git/gitlab
-    ```
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
-  and fill in the details for your specific IMAP server and email account:
-
-    ```sh
-    sudo editor config/gitlab.yml
-    ```
-
-    ```yaml
-    # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
-    incoming_email:
-      enabled: true
-
-      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
-      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
-      address: "incoming+%{key}@gitlab.example.com"
-
-      # Email account username
-      # With third party providers, this is usually the full email address.
-      # With self-hosted email servers, this is usually the user part of the email address.
-      user: "incoming"
-      # Email account password
-      password: "[REDACTED]"
-
-      # IMAP server host
-      host: "gitlab.example.com"
-      # IMAP server port
-      port: 143
-      # Whether the IMAP server uses SSL
-      ssl: false
-      # Whether the IMAP server uses StartTLS
-      start_tls: false
-
-      # The mailbox where incoming mail will end up. Usually "inbox".
-      mailbox: "inbox"
-    ```
-
-    ```yaml
-    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
-    incoming_email:
-      enabled: true
-
-      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
-      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
-      address: "gitlab-incoming+%{key}@gmail.com"
-
-      # Email account username
-      # With third party providers, this is usually the full email address.
-      # With self-hosted email servers, this is usually the user part of the email address.
-      user: "gitlab-incoming@gmail.com"
-      # Email account password
-      password: "[REDACTED]"
-
-      # IMAP server host
-      host: "imap.gmail.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"
-    ```
-
-1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
-
-    ```sh
-    sudo mkdir -p /etc/default
-    echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
-    ```
-
-1. Restart GitLab:
-
-    ```sh
-    sudo service gitlab restart
-    ```
-
-1. Verify that everything is configured correctly:
-
-    ```sh
-    sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
-    ```
-
-1. Reply by email should now be working.
-
-### Development
-
-1. Go to the GitLab installation directory.
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
-
-    ```yaml
-    # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
-    incoming_email:
-      enabled: true
-
-      # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
-      # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
-      address: "gitlab-incoming+%{key}@gmail.com"
-
-      # Email account username
-      # With third party providers, this is usually the full email address.
-      # With self-hosted email servers, this is usually the user part of the email address.
-      user: "gitlab-incoming@gmail.com"
-      # Email account password
-      password: "[REDACTED]"
-
-      # IMAP server host
-      host: "imap.gmail.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"
-    ```
-
-    As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-
-1. Uncomment the `mail_room` line in your `Procfile`:
-
-    ```yaml
-    mail_room: bundle exec mail_room -q -c config/mail_room.yml
-    ```
-
-1. Restart GitLab:
-
-    ```sh
-    bundle exec foreman start
-    ```
-
-1. Verify that everything is configured correctly:
-
-    ```sh
-    bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
-    ```
-
-1. Reply by email should now be working.
+This document was moved to [administration/reply_by_email](../administration/reply_by_email.md).
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 787d21f7f8fc1c0437a0280dd09c79ee2108a2e5..90833238ac5eaa53d04d1e407b2d748853114ba2 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -1,321 +1 @@
-# Set up Postfix for Reply by email
-
-This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email.
-
-The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
-
-## Configure your server firewall
-
-1. Open up port 25 on your server so that people can send email into the server over SMTP.
-2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
-
-## Install packages
-
-1. Install the `postfix` package if it is not installed already:
-
-    ```sh
-    sudo apt-get install postfix
-    ```
-
-    When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
-
-1. Install the `mailutils` package.
-
-    ```sh
-    sudo apt-get install mailutils
-    ```
-
-## Create user
-
-1. Create a user for incoming email.
-
-    ```sh
-    sudo useradd -m -s /bin/bash incoming
-    ```
-
-1. Set a password for this user.
-
-    ```sh
-    sudo passwd incoming
-    ```
-
-    Be sure not to forget this, you'll need it later.
-
-## Test the out-of-the-box setup
-
-1. Connect to the local SMTP server:
-    
-    ```sh
-    telnet localhost 25
-    ```
-
-    You should see a prompt like this:
-
-    ```sh
-    Trying 127.0.0.1...
-    Connected to localhost.
-    Escape character is '^]'.
-    220 gitlab.example.com ESMTP Postfix (Ubuntu)
-    ```
-
-    If you get a `Connection refused` error instead, verify that `postfix` is running:
-
-    ```sh
-    sudo postfix status
-    ```
-
-    If it is not, start it:
-
-    ```sh
-    sudo postfix start
-    ```
-
-1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-    
-    ```
-    ehlo localhost
-    mail from: root@localhost
-    rcpt to: incoming@localhost
-    data
-    Subject: Re: Some issue
-
-    Sounds good!
-    .
-    quit
-    ```
-
-    _**Note:** The `.` is a literal period on its own line._
-
-    _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
-    then your Postfix `my_network` configuration is not correct. The error will
-    say 'Temporary lookup failure'. See
-    [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
-
-1. Check if the `incoming` user received the email:
-    
-    ```sh
-    su - incoming
-    mail
-    ```
-
-    You should see output like this:
-
-    ```
-    "/var/mail/incoming": 1 message 1 unread
-    >U   1 root@localhost                           59/2842  Re: Some issue
-    ```
-
-    Quit the mail app:
-
-    ```sh
-    q
-    ```
-
-1. Log out of the `incoming` account and go back to being `root`:
-
-    ```sh
-    logout
-    ```
-
-## Configure Postfix to use Maildir-style mailboxes
-
-Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
-
-1. Configure Postfix to use Maildir-style mailboxes:
-    
-    ```sh
-    sudo postconf -e "home_mailbox = Maildir/"
-    ```
-
-1. Restart Postfix:
-    
-    ```sh
-    sudo /etc/init.d/postfix restart
-    ```
-
-1. Test the new setup:
-    
-    1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
-    1. Check if the `incoming` user received the email:
-    
-        ```sh
-        su - incoming
-        MAIL=/home/incoming/Maildir
-        mail
-        ```
-
-        You should see output like this:
-
-        ```
-        "/home/incoming/Maildir": 1 message 1 unread
-        >U   1 root@localhost                           59/2842  Re: Some issue
-        ```
-
-        Quit the mail app:
-
-        ```sh
-        q
-        ```
-
-    _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
-    version of `mail` doesn't support Maildir style mailboxes. Install
-    `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
-    try the above steps again, substituting `heirloom-mailx` for the `mail`
-    command._
-
-1. Log out of the `incoming` account and go back to being `root`:
-
-    ```sh
-    logout
-    ```
-
-## Install the Courier IMAP server
-    
-1. Install the `courier-imap` package:
-
-    ```sh
-    sudo apt-get install courier-imap
-    ```
-
-## Configure Postfix to receive email from the internet
-
-1. Let Postfix know about the domains that it should consider local:
-    
-    ```sh
-    sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
-    ```
-
-1. Let Postfix know about the IPs that it should consider part of the LAN:
-    
-    We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
-    
-    ```sh
-    sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
-    ```
-
-1. Configure Postfix to receive mail on all interfaces, which includes the internet:
-    
-    ```sh
-    sudo postconf -e "inet_interfaces = all"
-    ```
-
-1. Configure Postfix to use the `+` delimiter for sub-addressing:
-    
-    ```sh
-    sudo postconf -e "recipient_delimiter = +"
-    ```
-
-1. Restart Postfix:
-    
-    ```sh
-    sudo service postfix restart
-    ```
-
-## Test the final setup
-
-1. Test SMTP under the new setup:
-    
-    1. Connect to the SMTP server:
-        
-        ```sh
-        telnet gitlab.example.com 25
-        ```
-
-        You should see a prompt like this:
-
-        ```sh
-        Trying 123.123.123.123...
-        Connected to gitlab.example.com.
-        Escape character is '^]'.
-        220 gitlab.example.com ESMTP Postfix (Ubuntu)
-        ```
-
-        If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
-
-    1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
-        
-        ```
-        ehlo gitlab.example.com
-        mail from: root@gitlab.example.com
-        rcpt to: incoming@gitlab.example.com
-        data
-        Subject: Re: Some issue
-
-        Sounds good!
-        .
-        quit
-        ```
-
-        (Note: The `.` is a literal period on its own line)
-
-    1. Check if the `incoming` user received the email:
-    
-        ```sh
-        su - incoming
-        MAIL=/home/incoming/Maildir
-        mail
-        ```
-
-        You should see output like this:
-
-        ```
-        "/home/incoming/Maildir": 1 message 1 unread
-        >U   1 root@gitlab.example.com                           59/2842  Re: Some issue
-        ```
-
-        Quit the mail app:
-
-        ```sh
-        q
-        ```
-
-    1. Log out of the `incoming` account and go back to being `root`:
-
-        ```sh
-        logout
-        ```
-
-1. Test IMAP under the new setup:
-    
-    1. Connect to the IMAP server:
-        
-        ```sh
-        telnet gitlab.example.com 143
-        ```
-
-        You should see a prompt like this:
-
-        ```sh
-        Trying 123.123.123.123...
-        Connected to mail.example.gitlab.com.
-        Escape character is '^]'.
-        - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc.  See COPYING for distribution information.
-        ```
-
-    1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
-
-        ```
-        a login incoming PASSWORD
-        ```
-
-        Replace PASSWORD with the password you set on the `incoming` user earlier.
-
-        You should see output like this:
-
-        ```
-        a OK LOGIN Ok.
-        ```
-
-    1. Disconnect from the IMAP server:
-
-        ```sh
-        a logout
-        ```
-
-## Done!
-
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
-
----------
-
-_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
+This document was moved to [administration/reply_by_email_postfix_setup](../administration/reply_by_email_postfix_setup.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index eb9606934cd87312c383222373d5695d81aed6dc..b5e2640b3806cd616e88d0fd95a0720aacf8349c 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
 
 ## 2. Ruby
 
-_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
+**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
 
 The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
 in production, frequently leads to hard to diagnose problems. For example,
@@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
 use 64-bit Linux. You can find downloads for other platforms at the [Go download
 page](https://golang.org/dl).
 
+    # Remove former Go installation folder
+    sudo rm -rf /usr/local/go
+
     curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
     echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53  go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
       sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
@@ -268,9 +271,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-11-stable gitlab
+    sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab
 
-**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
 
 ### Configure It
 
@@ -331,6 +334,9 @@ sudo usermod -aG redis git
     # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed
     sudo -u git -H git config --global gc.auto 0
 
+    # Enable packfile bitmaps
+    sudo -u git -H git config --global repack.writeBitmaps true
+
     # Configure Redis connection settings
     sudo -u git -H cp config/resque.yml.example config/resque.yml
 
@@ -378,7 +384,7 @@ sudo usermod -aG redis git
 GitLab Shell is an SSH access and repository management software developed specially for GitLab.
 
     # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
-    sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+    sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true
 
     # By default, the gitlab-shell config is generated from your main GitLab config.
     # You can review (and modify) the gitlab-shell config as follows:
@@ -397,7 +403,7 @@ If you are not using Linux you may have to run `gmake` instead of
     cd /home/git
     sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
     cd gitlab-workhorse
-    sudo -u git -H git checkout v0.7.8
+    sudo -u git -H git checkout v1.0.0
     sudo -u git -H make
 
 ### Initialize Database and Activate Advanced Features
@@ -473,10 +479,14 @@ Copy the example site config:
     sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab
     sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab
 
-Make sure to edit the config file to match your setup:
+Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user:
 
     # Change YOUR_SERVER_FQDN to the fully-qualified
     # domain name of your host serving GitLab.
+    #
+    # Remember to match your paths to GitLab, especially
+    # if installing for a user other than 'git'.
+    #
     # If using Ubuntu default nginx install:
     # either remove the default_server from the listen line
     # or else sudo rm -f /etc/nginx/sites-enabled/default
@@ -560,7 +570,7 @@ Using a self-signed certificate is discouraged but if you must use it follow the
 
 ### Enable Reply by email
 
-See the ["Reply by email" documentation](../incoming_email/README.md) for more information on how to set this up.
+See the ["Reply by email" documentation](../administration/reply_by_email.md) for more information on how to set this up.
 
 ### LDAP Authentication
 
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index a65ac8a5f79adc5c6736de05efa39506f1e7ce9c..766a71199435d25f1ba073dd50d36eec9cc90a9c 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab.
 
 ## Ruby versions
 
-GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 or 2.3.
+GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13.
 
 You will have to use the standard MRI implementation of Ruby.
 We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
@@ -63,30 +63,30 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
 
 ### Memory
 
-You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab!
+You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab!
 The operating system and any other running applications will also be using memory
-so keep in mind that you need at least 2GB available before running GitLab. With
+so keep in mind that you need at least 4GB available before running GitLab. With
 less memory GitLab will give strange errors during the reconfigure run and 500
 errors during usage.
 
-- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
-- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow
-- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
-- 4GB RAM supports up to 1,000 users
-- 8GB RAM supports up to 2,000 users
-- 16GB RAM supports up to 4,000 users
-- 32GB RAM supports up to 8,000 users
-- 64GB RAM supports up to 16,000 users
-- 128GB RAM supports up to 32,000 users
+- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
+- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow
+- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
+- 8GB RAM supports up to 1,000 users
+- 16GB RAM supports up to 2,000 users
+- 32GB RAM supports up to 4,000 users
+- 64GB RAM supports up to 8,000 users
+- 128GB RAM supports up to 16,000 users
+- 256GB RAM supports up to 32,000 users
 - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
 
-We recommend having at least 1GB of swap on your server, even if you currently have
+We recommend having at least 2GB of swap on your server, even if you currently have
 enough available RAM. Having swap will help reduce the chance of errors occurring
 if your available memory changes.
 
 Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
 
-## Gitlab Runner
+## GitLab Runner
 
 We strongly advise against installing GitLab Runner on the same machine you plan
 to install GitLab on. Depending on how you decide to configure GitLab Runner and
@@ -113,10 +113,8 @@ It's possible to increase the amount of unicorn workers and this will usually he
 For most instances we recommend using: CPU cores + 1 = unicorn workers.
 So for a machine with 2 cores, 3 unicorn workers is ideal.
 
-For all machines that have 1GB and up we recommend a minimum of three unicorn workers.
-If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping.
-With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check).
-If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping.
+For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
+If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
 
 To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
 
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ddbd570ac6c9ed37b61e303be07adc7c66b26849..c2fd299db07a1240770433265f83ba42f2e73cca 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services.
 - [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
+- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
 
 GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
 
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 2eb6266ebe7952e8235ef09273711b343e567a3f..556d71b8b7643936db8688a2437647c4189cc12f 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -1,111 +1,164 @@
-# Integrate your server with Bitbucket
+# Integrate your GitLab server with Bitbucket
 
-Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
+Import projects from Bitbucket.org and login to your GitLab instance with your
+Bitbucket.org account.
 
-To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
-Bitbucket will generate an application ID and secret key for you to use.
+## Overview
 
-1.  Sign in to Bitbucket.
+You can set up Bitbucket.org as an OAuth provider so that you can use your
+credentials to authenticate into GitLab or import your projects from
+Bitbucket.org.
 
-1.  Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you.
+- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth
+  provider](#bitbucket-omniauth-provider) section.
+- To import projects from Bitbucket, follow both the
+  [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and
+  [Bitbucket project import](#bitbucket-project-import) sections.
 
-1.  Select "OAuth" in the left menu.
+## Bitbucket OmniAuth provider
 
-1.  Select "Add consumer".
+> **Note:**
+Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
+before proceeding with setting up the Bitbucket integration.
 
-1.  Provide the required details.
-    - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
-    - Application description: Fill this in if you wish.
-    - URL: The URL to your GitLab installation. 'https://gitlab.company.com'
-1.  Select "Save".
+To enable the Bitbucket OmniAuth provider you must register your application
+with Bitbucket.org. Bitbucket will generate an application ID and secret key for
+you to use.
 
-1.  You should now see a Key and Secret in the list of OAuth customers.
-    Keep this page open as you continue configuration.
+1.  Sign in to [Bitbucket.org](https://bitbucket.org).
+1.  Navigate to your individual user settings (**Bitbucket settings**) or a team's
+    settings (**Manage team**), depending on how you want the application registered.
+    It does not matter if the application is registered as an individual or a
+    team, that is entirely up to you.
+1.  Select **OAuth** in the left menu under "Access Management".
+1.  Select **Add consumer**.
+1.  Provide the required details:
 
-1.  On your GitLab server, open the configuration file.
+    | Item | Description |
+    | :--- | :---------- |
+    | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
+    | **Application description** | Fill this in if you wish. |
+    | **Callback URL** | Leave blank. |
+    | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
 
-    For omnibus package:
+    And grant at least the following permissions:
 
-    ```sh
-      sudo editor /etc/gitlab/gitlab.rb
+    ```
+    Account: Email
+    Repositories: Read, Admin
     ```
 
-    For installations from source:
+    >**Note:**
+    It may seem a little odd to giving GitLab admin permissions to repositories,
+    but this is needed in order for GitLab to be able to clone the repositories.
 
-    ```sh
-      cd /home/git/gitlab
+    ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
+
+1.  Select **Save**.
+1.  Select your newly created OAuth consumer and you should now see a Key and
+    Secret in the list of OAuth customers. Keep this page open as you continue
+    the configuration.
+
+      ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png)
+
+1.  On your GitLab server, open the configuration file:
 
-      sudo -u git -H editor config/gitlab.yml
     ```
+    # For Omnibus packages
+    sudo editor /etc/gitlab/gitlab.rb
 
-1.  See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+    # For installations from source
+    sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
+    ```
 
-1.  Add the provider configuration:
+1.  Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+    for initial settings.
+1.  Add the Bitbucket provider configuration:
 
-    For omnibus package:
+    For Omnibus packages:
 
     ```ruby
-      gitlab_rails['omniauth_providers'] = [
-        {
-          "name" => "bitbucket",
-          "app_id" => "YOUR_KEY",
-          "app_secret" => "YOUR_APP_SECRET",
-          "url" => "https://bitbucket.org/"
-        }
-      ]
+    gitlab_rails['omniauth_providers'] = [
+      {
+        "name" => "bitbucket",
+        "app_id" => "BITBUCKET_APP_KEY",
+        "app_secret" => "BITBUCKET_APP_SECRET",
+        "url" => "https://bitbucket.org/"
+      }
+    ]
     ```
 
-    For installation from source:
+    For installations from source:
 
-    ```
-      - { name: 'bitbucket', app_id: 'YOUR_KEY',
-        app_secret: 'YOUR_APP_SECRET' }
+    ```yaml
+    - { name: 'bitbucket',
+        app_id: 'BITBUCKET_APP_KEY',
+        app_secret: 'BITBUCKET_APP_SECRET' }
     ```
 
-1.  Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7.
+    ---
 
-1.  Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7.
+    Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret
+    from the Bitbucket application page.
 
 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.
 
-1.  If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```).
-
-1.  Restart GitLab for the changes to take effect.
-
-On the sign in page there should now be a Bitbucket icon below the regular sign in form.
-Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
-If everything goes well the user will be returned to GitLab and will be signed in.
+On the sign in page there should now be a Bitbucket icon below the regular sign
+in form. Click the icon to begin the authentication process. Bitbucket will ask
+the user to sign in and authorize the GitLab application. If everything goes
+well, the user will be returned to GitLab and will be signed in.
 
 ## Bitbucket project import
 
-To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
+To allow projects to be imported directly into GitLab, Bitbucket requires two
+extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
 
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
+Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
+instead requires GitLab to use SSH and identify itself using your GitLab
+server's SSH key.
 
-### Step 1: Public key
+To be able to access repositories on Bitbucket, GitLab will automatically
+register your public key with Bitbucket as a deploy key for the repositories to
+be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
+translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
+`/home/git/.ssh/bitbucket_rsa.pub` for installations from source.
 
-To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations.
+---
 
-If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
+Below are the steps that will allow GitLab to be able to import your projects
+from Bitbucket.
 
-1. Create a new SSH key:
+1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
+1. Create a new SSH key with an **empty passphrase**:
 
     ```sh
     sudo -u git -H ssh-keygen
     ```
 
-    When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`.
-    Make sure to use an **empty passphrase**.
+    When asked to 'Enter file in which to save the key' enter:
+    `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
+    `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
+    important so make sure to get it right.
 
-1. Configure SSH client to use your new key:
+    > **Warning:**
+    This key must NOT be associated with ANY existing Bitbucket accounts. If it
+    is, the import will fail with an `Access denied! Please verify you can add
+    deploy keys to this repository.` error.
 
-    Open the SSH configuration file of the git user.
+1. Next, you need to to configure the SSH client to use your new key. Open the
+   SSH configuration file of the `git` user:
 
-    ```sh
-      sudo editor /home/git/.ssh/config
+    ```
+    # For Omnibus packages
+    sudo editor /var/opt/gitlab/.ssh/config
+
+    # For installations from source
+    sudo editor /home/git/.ssh/config
     ```
 
-    Add a host configuration for `bitbucket.org`.
+1. Add a host configuration for `bitbucket.org`:
 
     ```sh
     Host bitbucket.org
@@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec
       User git
     ```
 
-### Step 2: Known hosts
-
-To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so:
-
-1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use:
+1. Save the file and exit.
+1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
+   user that GitLab will use:
 
     ```sh
     sudo -u git -H ssh bitbucket.org
     ```
 
-1.  Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter):
+    That step is performed because GitLab needs to connect to Bitbucket over SSH,
+    in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
+
+1.  Verify the RSA key fingerprint you'll see in the response matches the one
+    in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
+    doesn't matter):
 
     ```sh
-    The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established.
-    RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
+    The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
+    RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
     Are you sure you want to continue connecting (yes/no)?
     ```
 
-1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts.
+1. If the fingerprint matches, type `yes` to continue connecting and have
+   `bitbucket.org` be added to your known SSH hosts. After confirming you should
+   see a permission denied message. If you see an authentication successful
+   message you have done something wrong. The key you are using has already been
+   added to a Bitbucket account and will cause the import script to fail. Ensure
+   the key you are using CANNOT authenticate with Bitbucket.
+1. Restart GitLab to allow it to find the new public key.
 
-1. Your GitLab server is now able to connect to Bitbucket over SSH.
+Your GitLab server is now able to connect to Bitbucket over SSH. You should be
+able to see the "Import projects from Bitbucket" option on the New Project page
+enabled.
 
-1. Restart GitLab to allow it to find the new public key.
+## Acknowledgemts
+
+Special thanks to the writer behind the following article:
+
+- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
 
-You should now see the "Import projects from Bitbucket" option on the New Project page enabled.
+[init-oauth]: omniauth.md#initial-omniauth-configuration
+[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png
new file mode 100644
index 0000000000000000000000000000000000000000..3fb2f7524a3e24b4727cb4b888a8f884116e7534
Binary files /dev/null and b/doc/integration/img/bitbucket_oauth_keys.png differ
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3047712d8c1f670184e564c23329e2b653445be
Binary files /dev/null and b/doc/integration/img/bitbucket_oauth_settings_page.png differ
diff --git a/doc/integration/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ba737bda9a713c4ac57fe795c10b1af133a8dde
Binary files /dev/null and b/doc/integration/img/jira_add_user_to_group.png differ
diff --git a/doc/integration/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png
new file mode 100644
index 0000000000000000000000000000000000000000..0609060cb05c4d5827efdc9d685186ade64bbbf6
Binary files /dev/null and b/doc/integration/img/jira_create_new_group.png differ
diff --git a/doc/integration/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png
new file mode 100644
index 0000000000000000000000000000000000000000..53d77b17df0168ecb2e2f037129f865d0549edcd
Binary files /dev/null and b/doc/integration/img/jira_create_new_group_name.png differ
diff --git a/doc/integration/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png
new file mode 100644
index 0000000000000000000000000000000000000000..9eaa444ed25b9b17e12a311a77a36bba023368aa
Binary files /dev/null and b/doc/integration/img/jira_create_new_user.png differ
diff --git a/doc/integration/img/jira_group_access.png b/doc/integration/img/jira_group_access.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d4657427ae206efea0da2ee8a4b0a50196f576f
Binary files /dev/null and b/doc/integration/img/jira_group_access.png differ
diff --git a/doc/integration/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a2d9f04a6c6bca62bb4e79ee3293ddc22b99739
Binary files /dev/null and b/doc/integration/img/jira_issue_reference.png differ
diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8f6058a5144a06c07917166120555fda4fd80a0
Binary files /dev/null and b/doc/integration/img/jira_merge_request_close.png differ
diff --git a/doc/integration/img/jira_project_name.png b/doc/integration/img/jira_project_name.png
new file mode 100644
index 0000000000000000000000000000000000000000..e785ec6140d4c69943a7c705d886515ce6ad2fa2
Binary files /dev/null and b/doc/integration/img/jira_project_name.png differ
diff --git a/doc/integration/img/jira_service.png b/doc/integration/img/jira_service.png
new file mode 100644
index 0000000000000000000000000000000000000000..13aefce6f84628fa4e0bb05c674a96a6e12e4948
Binary files /dev/null and b/doc/integration/img/jira_service.png differ
diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png
new file mode 100644
index 0000000000000000000000000000000000000000..eed69e80d2c585939ccb82a37b2bb0a04b5ceb1c
Binary files /dev/null and b/doc/integration/img/jira_service_close_issue.png differ
diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..0cc160bebe2fda31497afd077b7cd0f5b0930dec
Binary files /dev/null and b/doc/integration/img/jira_service_page.png differ
diff --git a/doc/integration/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f002b59bac3ad76482355ac4534a1c297e3c636
Binary files /dev/null and b/doc/integration/img/jira_user_management_link.png differ
diff --git a/doc/integration/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..937a50a77d90e84e8bab18831ae0e0feed54b61e
Binary files /dev/null and b/doc/integration/img/jira_workflow_screenshot.png differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 46b260e7033b50e47a6e99ab787dbb63c124a915..8a55fce96fe2f6952abecbabf0f9784d5b0d33e3 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -102,8 +102,8 @@ To change these settings:
         block_auto_created_users: true
     ```
 
-Now we can choose one or more of the Supported Providers listed above to continue
-the configuration process.
+Now we can choose one or more of the [Supported Providers](#supported-providers)
+listed above to continue the configuration process.
 
 ## Enable OmniAuth for an Existing User
 
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index f3b2a2887769f6e3c36d24b71c61758743841af0..4a242c321aa8c3e71b4d9fb62e95987dbc190f83 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro
 the SAML request, but this error never reaches GitLab due to the CSRF check.
 
 To bypass this you can add `skip_before_action :verify_authenticity_token` to the
-`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
-where it can then be seen in the usual logs, or as a flash message in the login
-screen.
-
-That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
-for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
-installations from source.
+`omniauth_callbacks_controller.rb` file immediately after the `class` line and
+comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This
+will allow the error to hit GitLab, where it can then be seen in the usual logs,
+or as a flash message on the login screen.
+
+That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
+for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for
+installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn`
+command on Omnibus installations and `sudo service gitlab restart` on installations
+from source.
+
+You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer)
+(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace)
+(Chrome) browser extensions useful in your debugging.
 
 ### Invalid audience
 
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 1850031eb26452bb9566489de51032a56ff14955..1790b2b761f40adf11647b0b0672f2b621755fe6 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -22,10 +22,10 @@ Create merge requests and review code.
 
 - [Fork a project and contribute to it](../workflow/forking_workflow.md)
 - [Create a new merge request](../gitlab-basics/add-merge-request.md)
-- [Automatically close issues from merge requests](../customization/issue_closing.md)
-- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
-- [Revert any commit](../workflow/revert_changes.md)
-- [Cherry-pick any commit](../workflow/cherry_pick_changes.md)
+- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
+- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
+- [Revert any commit](../user/project/merge_requests/revert_changes.md)
+- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
 
 ## Test and Deploy
 
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index edd6c59138fd89dcebeb31abbd817731402ad29a..7f08188bd652eb8cd31d6bab307bfb92464df3ad 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
 
 Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
 
-4.  You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]."
+4.  You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
 
 5.  You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
 
@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
 
 7.  Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
 
-8.  It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V..
+8.  It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
 
 This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index eac57bc3de4b9da3e95b57c846f2a0bb85694139..6cf93c33ec20125374bb1da4cfdf71c2684404c2 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1,66 +1 @@
-# Health Check
-
-> [Introduced][ce-3888] in GitLab 8.8.
-
-GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
-endpoint. The health check reports on the overall system status based on the status of
-the database connection, the state of the database migrations, and the ability to write
-and access the cache. This endpoint can be provided to uptime monitoring services like
-[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
-
-## Access Token
-
-An access token needs to be provided while accessing the health check endpoint. The current
-accepted token can be found on the `admin/health_check` page of your GitLab instance.
-
-![access token](img/health_check_token.png)
-
-The access token can be passed as a URL parameter:
-
-```
-https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
-```
-
-or as an HTTP header:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-## Using the Endpoint
-
-Once you have the access token, health information can be retrieved as plain text, JSON,
-or XML using the `health_check` endpoint:
-
-- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
-
-You can also ask for the status of specific services:
-
-- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
-- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
-
-For example, the JSON output of the following health check:
-
-```bash
-curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
-```
-
-would be like:
-
-```
-{"healthy":true,"message":"success"}
-```
-
-## Status
-
-On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
-will return a valid successful HTTP status code, and a `success` message. Ideally your
-uptime monitoring should look for the success message.
-
-[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
-[pingdom]: https://www.pingdom.com
-[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
-[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
+This document was moved to [user/admin_area/monitoring/health_check](../user/admin_area/monitoring/health_check.md).
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
index 771584268d9106533765710e68154da4419b9566..19d4613593034b4bfc504382b2908fdf764e9555 100644
--- a/doc/monitoring/performance/gitlab_configuration.md
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -1,40 +1 @@
-# GitLab Configuration
-
-GitLab Performance Monitoring is disabled by default. To enable it and change any of its
-settings, navigate to the Admin area in **Settings > Metrics**
-(`/admin/application_settings`).
-
-The minimum required settings you need to set are the InfluxDB host and port.
-Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
-changes.
-
----
-
-![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
-
----
-
-Finally, a restart of all GitLab processes is required for the changes to take
-effect:
-
-```bash
-# For Omnibus installations
-sudo gitlab-ctl restart
-
-# For installations from source
-sudo service gitlab restart
-```
-
-## Pending Migrations
-
-When any migrations are pending, the metrics are disabled until the migrations
-have been performed.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/gitlab_configuration](../../administration/monitoring/performance/gitlab_configuration.md).
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 7947b0fedc4eeb19a385c569431523da7f4440d8..0d4be02ff5f03bac5d2a9dfbee5ab727c82881ef 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -1,111 +1 @@
-# Grafana Configuration
-
-[Grafana](http://grafana.org/) is a tool that allows you to visualize time
-series metrics through graphs and dashboards. It supports several backend
-data stores, including InfluxDB. GitLab writes performance data to InfluxDB
-and Grafana will allow you to query InfluxDB to display useful graphs.
-
-For the easiest installation and configuration, install Grafana on the same
-server as InfluxDB. For larger installations, you may want to split out these
-services.
-
-## Installation
-
-Grafana supplies package repositories (Yum/Apt) for easy installation.
-See [Grafana installation documentation](http://docs.grafana.org/installation/)
-for detailed steps.
-
-> **Note**: Before starting Grafana for the first time, set the admin user
-and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
-will be `admin`.
-
-## Configuration
-
-Login as the admin user. Expand the menu by clicking the Grafana logo in the
-top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
-in the top bar.
-
-![Grafana empty data source page](img/grafana_data_source_empty.png)
-
-Fill in the configuration details for the InfluxDB data source. Save and
-Test Connection to ensure the configuration is correct.
-
-- **Name**: InfluxDB
-- **Default**: Checked
-- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
-- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
-on a separate server)
-- **Access**: proxy
-- **Database**: gitlab
-- **User**: admin (Or the username configured when setting up InfluxDB)
-- **Password**: The password configured when you set up InfluxDB
-
-![Grafana data source configurations](img/grafana_data_source_configuration.png)
-
-## Apply retention policies and create continuous queries
-
-If you intend to import the GitLab provided Grafana dashboards, you will need to
-set up the right retention policies and continuous queries. The easiest way of
-doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
-repository.
-
-To use this repository you must first clone it:
-
-```
-git clone https://gitlab.com/gitlab-org/influxdb-management.git
-cd influxdb-management
-```
-
-Next you must install the required dependencies:
-
-```
-gem install bundler
-bundle install
-```
-
-Now you must configure the repository by first copying `.env.example` to `.env`
-and then editing the `.env` file to contain the correct InfluxDB settings. Once
-configured you can simply run `bundle exec rake` and the InfluxDB database will
-be configured for you.
-
-For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
-
-## Import Dashboards
-
-You can now import a set of default dashboards that will give you a good
-start on displaying useful information. GitLab has published a set of default
-[Grafana dashboards][grafana-dashboards] to get you started. Clone the
-repository or download a zip/tarball, then follow these steps to import each
-JSON file.
-
-Open the dashboard dropdown menu and click 'Import'
-
-![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
-
-Click 'Choose file' and browse to the location where you downloaded or cloned
-the dashboard repository. Pick one of the JSON files to import.
-
-![Grafana dashboard import](img/grafana_dashboard_import.png)
-
-Once the dashboard is imported, be sure to click save icon in the top bar. If
-you do not save the dashboard after importing it will be removed when you
-navigate away.
-
-![Grafana save icon](img/grafana_save_icon.png)
-
-Repeat this process for each dashboard you wish to import.
-
-Alternatively you can automatically import all the dashboards into your Grafana
-instance. See the README of the [Grafana dashboards][grafana-dashboards]
-repository for more information on this process.
-
-[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Installation/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
+This document was moved to [administration/monitoring/performance/grafana_configuration](../../administration/monitoring/performance/grafana_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
index c30cd2950d861bed573cfde89b35366ace2b3b24..15fd275e916281713c459c4c4b1d7f856fa072e3 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1,193 +1 @@
-# InfluxDB Configuration
-
-The default settings provided by [InfluxDB] are not sufficient for a high traffic
-GitLab environment. The settings discussed in this document are based on the
-settings GitLab uses for GitLab.com, depending on your own needs you may need to
-further adjust them.
-
-If you are intending to run InfluxDB on the same server as GitLab, make sure
-you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
-
-Unless you are going with a budget setup, it's advised to run it separately.
-
-## Requirements
-
-- InfluxDB 0.9.5 or newer
-- A fairly modern version of Linux
-- At least 4GB of RAM
-- At least 10GB of storage for InfluxDB data
-
-Note that the RAM and storage requirements can differ greatly depending on the
-amount of data received/stored. To limit the amount of stored data users can
-look into [InfluxDB Retention Policies][influxdb-retention].
-
-## Installation
-
-Installing InfluxDB is out of the scope of this document. Please refer to the
-[InfluxDB documentation].
-
-## InfluxDB Server Settings
-
-Since InfluxDB has many settings that users may wish to customize themselves
-(e.g. what port to run InfluxDB on), we'll only cover the essentials.
-
-The configuration file in question is usually located at
-`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
-InfluxDB needs to be restarted.
-
-### Storage Engine
-
-InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
-storage engine is available, called [TSM Tree]. All users **must** use the new
-`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
-upcoming InfluxDB releases.
-
-Make sure you have the following in your configuration file:
-
-```
-[data]
-  dir = "/var/lib/influxdb/data"
-  engine = "tsm1"
-```
-
-### Admin Panel
-
-Production environments should have the InfluxDB admin panel **disabled**. This
-feature can be disabled by adding the following to your InfluxDB configuration
-file:
-
-```
-[admin]
-  enabled = false
-```
-
-### HTTP
-
-HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
-thus it should be enabled. When enabling make sure to _also_ enable
-authentication:
-
-```
-[http]
-  enabled = true
-  auth-enabled = true
-```
-
-_**Note:** Before you enable authentication, you might want to [create an
-admin user](#create-a-new-admin-user)._
-
-### UDP
-
-GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
-UDP can be done using the following settings:
-
-```
-[[udp]]
-  enabled = true
-  bind-address = ":8089"
-  database = "gitlab"
-  batch-size = 1000
-  batch-pending = 5
-  batch-timeout = "1s"
-  read-buffer = 209715200
-```
-
-This does the following:
-
-1. Enable UDP and bind it to port 8089 for all addresses.
-2. Store any data received in the "gitlab" database.
-3. Define a batch of points to be 1000 points in size and allow a maximum of
-   5 batches _or_ flush them automatically after 1 second.
-4. Define a UDP read buffer size of 200 MB.
-
-One of the most important settings here is the UDP read buffer size as if this
-value is set too low, packets will be dropped. You must also make sure the OS
-buffer size is set to the same value, the default value is almost never enough.
-
-To set the OS buffer size to 200 MB, on Linux you can run the following command:
-
-```bash
-sysctl -w net.core.rmem_max=209715200
-```
-
-To make this permanent, add the following to `/etc/sysctl.conf` and restart the
-server:
-
-```bash
-net.core.rmem_max=209715200
-```
-
-It is **very important** to make sure the buffer sizes are large enough to
-handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
-buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
-traffic, users may be able to use a smaller buffer size, but we highly recommend
-using _at least_ 100 MB.
-
-When enabling UDP, users should take care to not expose the port to the public,
-as doing so will allow anybody to write data into your InfluxDB database (as
-[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
-whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
-allowing traffic from members of said VLAN.
-
-## Create a new admin user
-
-If you want to [enable authentication](#http), you might want to [create an
-admin user][influx-admin]:
-
-```
-influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
-```
-
-## Create the `gitlab` database
-
-Once you get InfluxDB up and running, you need to create a database for GitLab.
-Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
-before creating a database.
-
-_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
-[HTTP authentication](#http), remember to append the username (`-username <username>`)
-and password (`-password <password>`)  you set earlier to the commands below._
-
-Run the following command to create a database named `gitlab`:
-
-```bash
-influx -execute 'CREATE DATABASE gitlab'
-```
-
-The name **must** be `gitlab`, do not use any other name.
-
-Next, make sure that the database was successfully created:
-
-```bash
-influx -execute 'SHOW DATABASES'
-```
-
-The output should be similar to:
-
-```
-name: databases
----------------
-name
-_internal
-gitlab
-```
-
-That's it! Now your GitLab instance should send data to InfluxDB.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
-[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
-[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
-[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
-[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
-[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
+This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index 41861860b6d2d23ed9a5ec54fdefdcce18c6fcc0..e53f9701dc3fa8fa0f14a377c87e8de93e5ba26a 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1,88 +1 @@
-# InfluxDB Schema
-
-The following measurements are currently stored in InfluxDB:
-
-- `PROCESS_file_descriptors`
-- `PROCESS_gc_statistics`
-- `PROCESS_memory_usage`
-- `PROCESS_method_calls`
-- `PROCESS_object_counts`
-- `PROCESS_transactions`
-- `PROCESS_views`
-
-Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
-process type. In all series, any form of duration is stored in milliseconds.
-
-## PROCESS_file_descriptors
-
-This measurement contains the number of open file descriptors over time. The
-value field `value` contains the number of descriptors.
-
-## PROCESS_gc_statistics
-
-This measurement contains Ruby garbage collection statistics such as the amount
-of minor/major GC runs (relative to the last sampling interval), the time spent
-in garbage collection cycles, and all fields/values returned by `GC.stat`.
-
-## PROCESS_memory_usage
-
-This measurement contains the process' memory usage (in bytes) over time. The
-value field `value` contains the number of bytes.
-
-## PROCESS_method_calls
-
-This measurement contains the methods called during a transaction along with
-their duration, and a name of the transaction action that invoked the method (if
-available). The method call duration is stored in the value field `duration`,
-while the method name is stored in the tag `method`. The tag `action` contains
-the full name of the transaction action. Both the `method` and `action` fields
-are in the following format:
-
-```
-ClassName#method_name
-```
-
-For example, a method called by the `show` method in the `UsersController` class
-would have `action` set to `UsersController#show`.
-
-## PROCESS_object_counts
-
-This measurement is used to store retained Ruby objects (per class) and the
-amount of retained objects. The number of objects is stored in the `count` value
-field while the class name is stored in the `type` tag.
-
-## PROCESS_transactions
-
-This measurement is used to store basic transaction details such as the time it
-took to complete a transaction, how much time was spent in SQL queries, etc. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration`  | The total duration of the transaction |
-| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
-| `method_duration` | The total time spent in method calls |
-| `sql_duration` | The total time spent in SQL queries |
-| `view_duration` | The total time spent in views |
-
-## PROCESS_views
-
-This measurement is used to store view rendering timings for a transaction. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The rendering time of the view |
-| `view` | The path of the view, relative to the application's root directory |
-
-The `action` tag contains the action name of the transaction that rendered the
-view.
-
----
-
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](introduction.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md).
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index 79904916b7e2bdb08719bf1d3186e0cc337f6e6f..ae88baa0c14aea1cddbcd9c574bf0b49b568c74f 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1,65 +1 @@
-# GitLab Performance Monitoring
-
-GitLab comes with its own application performance measuring system as of GitLab
-8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
-Community and Enterprise editions.
-
-Apart from this introduction, you are advised to read through the following
-documents in order to understand and properly configure GitLab Performance Monitoring:
-
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Install/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
-
-## Introduction to GitLab Performance Monitoring
-
-GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
-including (but not limited to):
-
-- The time it took to complete a transaction (a web request or Sidekiq job).
-- The time spent in running SQL queries and rendering HAML views.
-- The time spent executing (instrumented) Ruby methods.
-- Ruby object allocations, and retained objects in particular.
-- System statistics such as the process' memory usage and open file descriptors.
-- Ruby garbage collection statistics.
-
-Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
-data can be visualized using [Grafana][grafana] or any other application that
-supports reading data from InfluxDB. Alternatively data can be queried using the
-InfluxDB CLI.
-
-## Metric Types
-
-Two types of metrics are collected:
-
-1. Transaction specific metrics.
-1. Sampled metrics, collected at a certain interval in a separate thread.
-
-### Transaction Metrics
-
-Transaction metrics are metrics that can be associated with a single
-transaction. This includes statistics such as the transaction duration, timings
-of any executed SQL queries, time spent rendering HAML views, etc. These metrics
-are collected for every Rack request and Sidekiq job processed.
-
-### Sampled Metrics
-
-Sampled metrics are metrics that can't be associated with a single transaction.
-Examples include garbage collection statistics and retained Ruby objects. These
-metrics are collected at a regular interval. This interval is made up out of two
-parts:
-
-1. A user defined interval.
-1. A randomly generated offset added on top of the interval, the same offset
-   can't be used twice in a row.
-
-The actual interval can be anywhere between a half of the defined interval and a
-half above the interval. For example, for a user defined interval of 15 seconds
-the actual interval can be anywhere between 7.5 and 22.5. The interval is
-re-generated for every sampling run instead of being generated once and re-used
-for the duration of the process' lifetime.
-
-[influxdb]: https://influxdata.com/time-series-platform/influxdb/
-[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
-[grafana]: http://grafana.org/
+This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md).
diff --git a/doc/operations/README.md b/doc/operations/README.md
index 6a35dab7b6c7a096ab16cce743a2e15a477a8685..58f16aff7bdb815b7be8fbe40e1b0c3bdbf028e8 100644
--- a/doc/operations/README.md
+++ b/doc/operations/README.md
@@ -1,5 +1 @@
-# GitLab operations
-
-- [Sidekiq MemoryKiller](sidekiq_memory_killer.md)
-- [Cleaning up Redis sessions](cleaning_up_redis_sessions.md)
-- [Understanding Unicorn and unicorn-worker-killer](unicorn.md)
+This document was moved to [administration/operations](../administration/operations.md).
diff --git a/doc/operations/cleaning_up_redis_sessions.md b/doc/operations/cleaning_up_redis_sessions.md
index 93521e976d51e05204c34addbedc8cb0e4c514ab..2a1d0a8c8eb646ad4e3072882f8aa4dcba02bedc 100644
--- a/doc/operations/cleaning_up_redis_sessions.md
+++ b/doc/operations/cleaning_up_redis_sessions.md
@@ -1,52 +1 @@
-# Cleaning up stale Redis sessions
-
-Since version 6.2, GitLab stores web user sessions as key-value pairs in Redis.
-Prior to GitLab 7.3, user sessions did not automatically expire from Redis. If
-you have been running a large GitLab server (thousands of users) since before
-GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis
-database after you upgrade to GitLab 7.3. You can also perform a cleanup while
-still running GitLab 7.2 or older, but in that case new stale sessions will
-start building up again after you clean up.
-
-In GitLab versions prior to 7.3.0, the session keys in Redis are 16-byte
-hexadecimal values such as '976aa289e2189b17d7ef525a6702ace9'. Starting with
-GitLab 7.3.0, the keys are
-prefixed with 'session:gitlab:', so they would look like
-'session:gitlab:976aa289e2189b17d7ef525a6702ace9'. Below we describe how to
-remove the keys in the old format.
-
-First we define a shell function with the proper Redis connection details.
-
-```
-rcli() {
-  # This example works for Omnibus installations of GitLab 7.3 or newer. For an
-  # installation from source you will have to change the socket path and the
-  # path to redis-cli.
-  sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
-}
-
-# test the new shell function; the response should be PONG
-rcli ping
-```
-
-Now we do a search to see if there are any session keys in the old format for
-us to clean up.
-
-```
-# returns the number of old-format session keys in Redis
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | wc -l
-```
-
-If the number is larger than zero, you can proceed to expire the keys from
-Redis. If the number is zero there is nothing to clean up.
-
-```
-# Tell Redis to expire each matched key after 600 seconds.
-rcli keys '*' | grep '^[a-f0-9]\{32\}$' | awk '{ print "expire", $0, 600 }' | rcli
-# This will print '(integer) 1' for each key that gets expired.
-```
-
-Over the next 15 minutes (10 minutes expiry time plus 5 minutes Redis
-background save interval) your Redis database will be compacted. If you are
-still using GitLab 7.2, users who are not clicking around in GitLab during the
-10 minute expiry window will be signed out of GitLab.
+This document was moved to [administration/operations/cleaning_up_redis_sessions](../administration/operations/cleaning_up_redis_sessions.md).
diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md
index 54adb99386a48f326d5e270c2f4ab23bc531a7b5..c54bca324a5d9d5c1103b5014017015f7b585c87 100644
--- a/doc/operations/moving_repositories.md
+++ b/doc/operations/moving_repositories.md
@@ -1,180 +1 @@
-# Moving repositories managed by GitLab
-
-Sometimes you need to move all repositories managed by GitLab to
-another filesystem or another server. In this document we will look
-at some of the ways you can copy all your repositories from
-`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
-
-We will look at three scenarios: the target directory is empty, the
-target directory contains an outdated copy of the repositories, and
-how to deal with thousands of repositories.
-
-**Each of the approaches we list can/will overwrite data in the
-target directory `/mnt/gitlab/repositories`. Do not mix up the
-source and the target.**
-
-## Target directory is empty: use a tar pipe
-
-If the target directory `/mnt/gitlab/repositories` is empty the
-simplest thing to do is to use a tar pipe.  This method has low
-overhead and tar is almost always already installed on your system.
-However, it is not possible to resume an interrupted tar pipe:  if
-that happens then all data must be copied again.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
-  tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to see progress, replace `-xf` with `-xvf`.
-
-### Tar pipe to another server
-
-You can also use a tar pipe to copy data to another server. If your
-'git' user has SSH access to the newserver as 'git@newserver', you
-can pipe the data through SSH.
-
-```
-# As the git user
-tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
-  ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
-```
-
-If you want to compress the data before it goes over the network
-(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
-
-## The target directory contains an outdated copy of the repositories: use rsync
-
-If the target directory already contains a partial / outdated copy
-of the repositories it may be wasteful to copy all the data again
-with tar. In this scenario it is better to use rsync. This utility
-is either already installed on your system or easily installable
-via apt, yum etc.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
-  /mnt/gitlab/repositories
-```
-
-The `/.` in the command above is very important, without it you can
-easily get the wrong directory structure in the target directory.
-If you want to see progress, replace `-a` with `-av`.
-
-### Single rsync to another server
-
-If the 'git' user on your source system has SSH access to the target
-server you can send the repositories over the network with rsync.
-
-```
-# As the 'git' user
-rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
-  git@newserver:/mnt/gitlab/repositories
-```
-
-## Thousands of Git repositories: use one rsync per repository
-
-Every time you start an rsync job it has to inspect all files in
-the source directory, all files in the target directory, and then
-decide what files to copy or not. If the source or target directory
-has many contents this startup phase of rsync can become a burden
-for your GitLab server. In cases like this you can make rsync's
-life easier by dividing its work in smaller pieces, and sync one
-repository at a time.
-
-In addition to rsync we will use [GNU
-Parallel](http://www.gnu.org/software/parallel/). This utility is
-not included in GitLab so you need to install it yourself with apt
-or yum.  Also note that the GitLab scripts we used below were added
-in GitLab 8.1.
-
-** This process does not clean up repositories at the target location that no
-longer exist at the source. ** If you start using your GitLab instance with
-`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
-after switching to the new repository storage directory.
-
-### Parallel rsync for all repositories known to GitLab
-
-This will sync repositories with 10 rsync processes at a time. We keep
-track of progress so that the transfer can be restarted if necessary.
-
-First we create a new directory, owned by 'git', to hold transfer
-logs. We assume the directory is empty before we start the transfer
-procedure, and that we are the only ones writing files in it.
-
-```
-# Omnibus
-sudo mkdir /var/opt/gitlab/transfer-logs
-sudo chown git:git /var/opt/gitlab/transfer-logs
-
-# Source
-sudo -u git -H mkdir /home/git/transfer-logs
-```
-
-We seed the process with a list of the directories we want to copy.
-
-```
-# Omnibus
-sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
-```
-
-Now we can start the transfer. The command below is idempotent, and
-the number of jobs done by GNU Parallel should converge to zero. If it
-does not some repositories listed in all-repos-1234.txt may have been
-deleted/renamed before they could be copied.
-
-```
-# Omnibus
-sudo -u git sh -c '
-cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
-  /usr/bin/env JOBS=10 \
-  /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
-    /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
-    /var/opt/gitlab/git-data/repositories \
-    /mnt/gitlab/repositories
-'
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H sh -c '
-cat /home/git/transfer-logs/* | sort | uniq -u |\
-  /usr/bin/env JOBS=10 \
-  bin/parallel-rsync-repos \
-    /home/git/transfer-logs/success-$(date +%s).log \
-    /home/git/repositories \
-    /mnt/gitlab/repositories
-`
-```
-
-### Parallel rsync only for repositories with recent activity
-
-Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
-Then you might only want to sync repositories that were changed via GitLab
-_after_ that time. You can use the 'SINCE' variable to tell 'rake
-gitlab:list_repos' to only print repositories with recent activity.
-
-```
-# Omnibus
-sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
-  sudo -u git \
-  /usr/bin/env JOBS=10 \
-  /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
-    success-$(date +%s).log \
-    /var/opt/gitlab/git-data/repositories \
-    /mnt/gitlab/repositories
-
-# Source
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
-  sudo -u git -H \
-  /usr/bin/env JOBS=10 \
-  bin/parallel-rsync-repos \
-    success-$(date +%s).log \
-    /home/git/repositories \
-    /mnt/gitlab/repositories
-```
+This document was moved to [administration/operations/moving_repositories](../administration/operations/moving_repositories.md).
diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md
index b5e783489898ab379f85241fe028e1f6add7e211..cf7c3b2e2ede52d99aa2b4eb177212baab92b872 100644
--- a/doc/operations/sidekiq_memory_killer.md
+++ b/doc/operations/sidekiq_memory_killer.md
@@ -1,40 +1 @@
-# Sidekiq MemoryKiller
-
-The GitLab Rails application code suffers from memory leaks. For web requests
-this problem is made manageable using
-[unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) which
-restarts Unicorn worker processes in between requests when needed. The Sidekiq
-MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
-to process background jobs.
-
-Unlike unicorn-worker-killer, which is enabled by default for all GitLab
-installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
-_only_ for Omnibus packages. The reason for this is that the MemoryKiller
-relies on Runit to restart Sidekiq after a memory-induced shutdown and GitLab
-installations from source do not all use Runit or an equivalent.
-
-With the default settings, the MemoryKiller will cause a Sidekiq restart no
-more often than once every 15 minutes, with the restart causing about one
-minute of delay for incoming background jobs.
-
-## Configuring the MemoryKiller
-
-The MemoryKiller is controlled using environment variables.
-
-- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is
-  greater than 0, then after each Sidekiq job, the MemoryKiller will check the
-  RSS of the Sidekiq process that executed the job. If the RSS of the Sidekiq
-  process (expressed in kilobytes) exceeds SIDEKIQ_MEMORY_KILLER_MAX_RSS, a
-  delayed shutdown is triggered. The default value for Omnibus packages is set
-  [in the omnibus-gitlab
-  repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
-  a shutdown is triggered, the Sidekiq process will keep working normally for
-  another 15 minutes.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
-  time has expired, the MemoryKiller tells Sidekiq to stop accepting new jobs.
-  Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
-  Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
-  restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
-  the final signal sent to the Sidekiq process when we want it to shut down.
+This document was moved to [administration/operations/sidekiq_memory_killer](../administration/operations/sidekiq_memory_killer.md).
diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md
index bad61151bda1ff45f7fb7a57a7f18026047f7b83..fbc9697b755f9b4cecbd38059e04ee12e777f0fd 100644
--- a/doc/operations/unicorn.md
+++ b/doc/operations/unicorn.md
@@ -1,86 +1 @@
-# Understanding Unicorn and unicorn-worker-killer
-
-## Unicorn
-
-GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web
-server, to handle web requests (web browsers and Git HTTP clients). Unicorn is
-a daemon written in Ruby and C that can load and run a Ruby on Rails
-application; in our case the Rails application is GitLab Community Edition or
-GitLab Enterprise Edition.
-
-Unicorn has a multi-process architecture to make better use of available CPU
-cores (processes can run on different cores) and to have stronger fault
-tolerance (most failures stay isolated in only one process and cannot take down
-GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby
-environment with the GitLab application code, and then spawns 'workers' which
-inherit this clean initial environment. The 'master' never handles any
-requests, that is left to the workers. The operating system network stack
-queues incoming requests and distributes them among the workers.
-
-In a perfect world, the master would spawn its pool of workers once, and then
-the workers handle incoming web requests one after another until the end of
-time. In reality, worker processes can crash or time out: if the master notices
-that a worker takes too long to handle a request it will terminate the worker
-process with SIGKILL ('kill -9'). No matter how the worker process ended, the
-master process will replace it with a new 'clean' process again. Unicorn is
-designed to be able to replace 'crashed' workers without dropping user
-requests.
-
-This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The
-master process has PID 56227 below.
-
-```
-[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
-[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
-[2015-06-05T10:58:08.708141 #62538]  INFO -- : worker=10 spawned pid=62538
-[2015-06-05T10:58:08.708824 #62538]  INFO -- : worker=10 ready
-```
-
-### Tunables
-
-The main tunables for Unicorn are the number of worker processes and the
-request timeout after which the Unicorn master terminates a worker process.
-See the [omnibus-gitlab Unicorn settings
-documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md)
-if you want to adjust these settings.
-
-## unicorn-worker-killer
-
-GitLab has memory leaks. These memory leaks manifest themselves in long-running
-processes, such as Unicorn workers. (The Unicorn master process is not known to
-leak memory, probably because it does not handle user requests.)
-
-To make these memory leaks manageable, GitLab comes with the
-[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
-gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
-workers to do a memory self-check after every 16 requests. If the memory of the
-Unicorn worker exceeds a pre-set limit then the worker process exits. The
-Unicorn master then automatically replaces the worker process.
-
-This is a robust way to handle memory leaks: Unicorn is designed to handle
-workers that 'crash' so no user requests will be dropped. The
-unicorn-worker-killer gem is designed to only terminate a worker process _in
-between requests_, so no user requests are affected.
-
-This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
-You see that worker 4 (PID 125918) is inspecting itself and decides to exit.
-The threshold memory value was 254802235 bytes, about 250MB. With GitLab this
-threshold is a random value between 200 and 250 MB.  The master process (PID
-117565) then reaps the worker process and spawns a new 'worker 4' with PID
-127549.
-
-```
-[2015-06-05T12:07:41.828374 #125918]  WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
-[2015-06-05T12:07:41.828472 #125918]  WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
-[2015-06-05T12:07:42.025916 #117565]  INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
-[2015-06-05T12:07:42.034527 #127549]  INFO -- : worker=4 spawned pid=127549
-[2015-06-05T12:07:42.035217 #127549]  INFO -- : worker=4 ready
-```
-
-One other thing that stands out in the log snippet above, taken from
-GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
-is a normal value for our current GitLab.com setup and traffic.
-
-The high frequency of Unicorn memory restarts on some GitLab sites can be a
-source of confusion for administrators. Usually they are a [red
-herring](https://en.wikipedia.org/wiki/Red_herring).
+This document was moved to [administration/operations/unicorn](../administration/operations/unicorn.md).
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index 82505b13401dbce5957d2e5011d99f14bfd94130..3f6dfe03d14aec74c01e2eb1b3d6e79d30293fab 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -117,6 +117,22 @@ Click on **Authenticate via U2F Device** to complete the process.
 This will clear all your two-factor authentication registrations, including mobile
 applications and U2F devices.
 
+## Personal access tokens
+
+When 2FA is enabled, you can no longer use your normal account password to
+authenticate with Git over HTTPS on the command line, you must use a personal
+access token instead.
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Access Tokens**.
+1. Choose a name and expiry date for the token.
+1. Click on **Create Personal Access Token**. 
+1. Save the personal access token somewhere safe.
+
+When using git over HTTPS on the command line, enter the personal access token
+into the password field.
+
 ## Note to GitLab administrators
 
 You need to take special care to that 2FA keeps working after
diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png
index 42084717ebe249e49fd9ac6574ef65f2868bb2ab..287587609a1aee884767e62dd6066847a60f443b 100644
Binary files a/doc/raketasks/backup_hrz.png and b/doc/raketasks/backup_hrz.png differ
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 835af5443a329bd05f46fea2fdd5ee0c591d484f..0ad84705cfdd08bc7814d66193e086f30126a98d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -2,34 +2,51 @@
 
 ![backup banner](backup_hrz.png)
 
-## Create a backup of the GitLab system
-
-A backup creates an archive file that contains the database, all repositories and all attachments.
-This archive will be saved in backup_path (see `config/gitlab.yml`).
-The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
-You can only restore a backup to exactly the same version of GitLab that you created it
-on, for example 7.2.1. The best way to migrate your repositories from one server to
+An application data backup creates an archive file that contains the database,
+all repositories and all attachments.
+This archive will be saved in `backup_path`, which is specified in the
+`config/gitlab.yml` file.
+The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
+identifies the time at which each backup was created.
+
+You can only restore a backup to exactly the same version of GitLab on which it
+was created.  The best way to migrate your repositories from one server to
 another is through backup restore.
 
-You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and
-`/etc/gitlab/gitlab.rb` (for omnibus packages) or
-`/home/git/gitlab/config/secrets.yml` (for installations from source). This file
-contains the database encryption keys used for two-factor authentication and CI
-secret variables, among other things. If you restore a GitLab backup without
-restoring the database encryption key, users who have two-factor authentication
-enabled will lose access to your GitLab server.
+To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
+(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
+from source). This file contains the database encryption key and CI secret
+variables used for two-factor authentication. If you fail to restore this
+encryption key file along with the application data backup, users with two-factor
+authentication enabled will lose access to your GitLab server.
+
+## Create a backup of the GitLab system
 
+Use this command if you've installed GitLab with the Omnibus package:
 ```
-# use this command if you've installed GitLab with the Omnibus package
 sudo gitlab-rake gitlab:backup:create
-
-# if you've installed GitLab from source
+```
+Use this if you've installed GitLab from source:
+```
 sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
 ```
+If you are running GitLab within a Docker container, you can run the backup from the host:
+```
+docker -t exec <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 build output logs)
+- `artifacts` (CI build artifacts)
+- `lfs` (LFS objects)
+- `registry` (Container Registry images)
 
-Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
-Use a comma to specify several options at the same time.
+Separate multiple data types to skip using a comma. For example:
 
 ```
 sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
@@ -68,8 +85,11 @@ Deleting old backups... [SKIPPING]
 
 Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
 It uses the [Fog library](http://fog.io/) to perform the upload.
-In the example below we use Amazon S3 for storage.
-But Fog also lets you use [other storage providers](http://fog.io/storage/).
+In the example below we use Amazon S3 for storage, but Fog also lets you use
+[other storage providers](http://fog.io/storage/). GitLab
+[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
+for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is
+[also available](#uploading-to-locally-mounted-shares).
 
 For omnibus packages:
 
@@ -79,6 +99,9 @@ gitlab_rails['backup_upload_connection'] = {
   'region' => 'eu-west-1',
   'aws_access_key_id' => 'AKIAKIAKI',
   'aws_secret_access_key' => 'secret123'
+  # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+  # ie. 'aws_access_key_id' => '',
+  # 'use_iam_profile' => 'true'
 }
 gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
 ```
@@ -95,6 +118,9 @@ For installations from source:
         region: eu-west-1
         aws_access_key_id: AKIAKIAKI
         aws_secret_access_key: 'secret123'
+        # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+        # ie. aws_access_key_id: ''
+        # use_iam_profile: 'true'
       # The remote 'directory' to store your backups. For S3, this would be the bucket name.
       remote_directory: 'my.s3.bucket'
       # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
@@ -155,7 +181,7 @@ with the name of your bucket:
 ### Uploading to locally mounted shares
 
 You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
-using the [`Local`](https://github.com/fog/fog-local#usage) storage provider.
+using the Fog [`Local`](https://github.com/fog/fog-local#usage) storage provider.
 The directory pointed to by the `local_root` key **must** be owned by the `git`
 user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
 `SMB`) or the user that you are executing the backup tasks under (for omnibus
@@ -222,7 +248,7 @@ of using encryption in the first place!
 
 If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
 If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
 
 At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
 `/etc/gitlab/gitlab-secrets.json` (Omnibus), or
diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md
index 3ff3fee6a40335d256a810d30054a67df6e50afd..f7f6a40cd047eccb798f255ea9982fe13b0b1038 100644
--- a/doc/raketasks/check.md
+++ b/doc/raketasks/check.md
@@ -1,63 +1,3 @@
 # Check Rake Tasks
 
-## Repository Integrity
-
-Even though Git is very resilient and tries to prevent data integrity issues,
-there are times when things go wrong. The following Rake tasks intend to
-help GitLab administrators diagnose problem repositories so they can be fixed.
-
-There are 3 things that are checked to determine integrity.
-
-1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
-   This step verifies the connectivity and validity of objects in the repository.
-1. Check for `config.lock` in the repository directory.
-1. Check for any branch/references lock files in `refs/heads`.
-
-It's important to note that the existence of `config.lock` or reference locks
-alone do not necessarily indicate a problem. Lock files are routinely created
-and removed as Git and GitLab perform operations on the repository. They serve
-to prevent data integrity issues. However, if a Git operation is interrupted these
-locks may not be cleaned up properly.
-
-The following symptoms may indicate a problem with repository integrity. If users
-experience these symptoms you may use the rake tasks described below to determine
-exactly which repositories are causing the trouble.
-
-- Receiving an error when trying to push code - `remote: error: cannot lock ref`
-- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
-
-### Check all GitLab repositories
-
-This task loops through all repositories on the GitLab server and runs the
-3 integrity checks described previously.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:repo:check
-
-# installation from source
-bundle exec rake gitlab:repo:check RAILS_ENV=production
-```
-
-### Check repositories for a specific user
-
-This task checks all repositories that a specific user has access to. This is important
-because sometimes you know which user is experiencing trouble but you don't know
-which project might be the cause.
-
-If the rake task is executed without brackets at the end, you will be prompted
-to enter a username.
-
-```bash
-# omnibus-gitlab
-sudo gitlab-rake gitlab:user:check_repos
-sudo gitlab-rake gitlab:user:check_repos[<username>]
-
-# installation from source
-bundle exec rake gitlab:user:check_repos RAILS_ENV=production
-bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
-```
-
-Example output:
-
-![gitlab:user:check_repos output](check_repos_output.png)
+This document was moved to [administration/raketasks/check](../administration/raketasks/check.md).
diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md
index 315cb56a089b6aa0aff79bf3a3c58e7a6e7aff1f..266aeb7d60e6f2eab6ef4f89dde7a732eaf46036 100644
--- a/doc/raketasks/maintenance.md
+++ b/doc/raketasks/maintenance.md
@@ -1,188 +1,3 @@
-# Maintenance
+# Maintenance Rake Tasks
 
-## Gather information about GitLab and the system it runs on
-
-This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:env:info
-
-# installation from source
-bundle exec rake gitlab:env:info RAILS_ENV=production
-```
-
-Example output:
-
-```
-System information
-System:           Debian 7.8
-Current User:     git
-Using RVM:        no
-Ruby Version:     2.1.5p273
-Gem Version:      2.4.3
-Bundler Version:  1.7.6
-Rake Version:     10.3.2
-Sidekiq Version:  2.17.8
-
-GitLab information
-Version:          7.7.1
-Revision:         41ab9e1
-Directory:        /home/git/gitlab
-DB Adapter:       postgresql
-URL:              https://gitlab.example.com
-HTTP Clone URL:   https://gitlab.example.com/some-project.git
-SSH Clone URL:    git@gitlab.example.com:some-project.git
-Using LDAP:       no
-Using Omniauth:   no
-
-GitLab Shell
-Version:          2.4.1
-Repositories:     /home/git/repositories/
-Hooks:            /home/git/gitlab-shell/hooks/
-Git:              /usr/bin/git
-```
-
-## Check GitLab configuration
-
-Runs the following rake tasks:
-
-- `gitlab:gitlab_shell:check`
-- `gitlab:sidekiq:check`
-- `gitlab:app:check`
-
-It will check that each component was setup according to the installation guide and suggest fixes for issues found.
-
-You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:check
-
-# installation from source
-bundle exec rake gitlab:check RAILS_ENV=production
-```
-
-NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
-
-Example output:
-
-```
-Checking Environment ...
-
-Git configured for git user? ... yes
-Has python2? ... yes
-python2 is supported version? ... yes
-
-Checking Environment ... Finished
-
-Checking GitLab Shell ...
-
-GitLab Shell version? ... OK (1.2.0)
-Repo base directory exists? ... yes
-Repo base directory is a symlink? ... no
-Repo base owned by git:git? ... yes
-Repo base access is drwxrws---? ... yes
-post-receive hook up-to-date? ... yes
-post-receive hooks in repos are links: ... yes
-
-Checking GitLab Shell ... Finished
-
-Checking Sidekiq ...
-
-Running? ... yes
-
-Checking Sidekiq ... Finished
-
-Checking GitLab ...
-
-Database config exists? ... yes
-Database is SQLite ... no
-All migrations up? ... yes
-GitLab config exists? ... yes
-GitLab config outdated? ... no
-Log directory writable? ... yes
-Tmp directory writable? ... yes
-Init script exists? ... yes
-Init script up-to-date? ... yes
-Redis version >= 2.0.0? ... yes
-
-Checking GitLab ... Finished
-```
-
-## Rebuild authorized_keys file
-
-In some case it is necessary to rebuild the `authorized_keys` file.
-
-For Omnibus-packages:
-```
-sudo gitlab-rake gitlab:shell:setup
-```
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
-```
-
-```
-This will rebuild an authorized_keys file.
-You will lose any data stored in authorized_keys file.
-Do you want to continue (yes/no)? yes
-```
-
-## Clear redis cache
-
-If for some reason the dashboard shows wrong information you might want to
-clear Redis' cache.
-
-For Omnibus-packages:
-```
-sudo gitlab-rake cache:clear
-```
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
-```
-
-## Precompile the assets
-
-Sometimes during version upgrades you might end up with some wrong CSS or
-missing some icons. In that case, try to precompile the assets again.
-
-Note that this only applies to source installations and does NOT apply to
-omnibus packages.
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
-```
-
-For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
-the release of upstream GitLab. The omnibus version includes optimized versions
-of those assets. Unless you are modifying the JavaScript / CSS code on your
-production machine after installing the package, there should be no reason to redo
-rake assets:precompile on the production machine. If you suspect that assets
-have been corrupted, you should reinstall the omnibus package.
-
-## Tracking Deployments
-
-GitLab provides a Rake task that lets you track deployments in GitLab
-Performance Monitoring. This Rake task simply stores the current GitLab version
-in the GitLab Performance Monitoring database.
-
-For Omnibus-packages:
-
-```
-sudo gitlab-rake gitlab:track_deployment
-```
-
-For installations from source:
-
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
-```
+This document was moved to [administration/raketasks/maintenance](../administration/raketasks/maintenance.md).
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 8a5e2d6e16bfe94e4d194dc421411ce10e2a9dfc..044b104f5c2c067ed653726fa2c9e8526840b92a 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -70,3 +70,18 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
 # installation from source
 bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
 ```
+
+## Clear authentication tokens for all users. Important! Data loss!
+
+Clear authentication tokens for all users in the GitLab database. This
+task is useful if your users' authentication tokens might have been exposed in
+any way. All the existing tokens will become invalid, and new tokens are
+automatically generated upon sign-in or user modification.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
+
+# installation from source
+bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
+```
diff --git a/doc/university/README.md b/doc/university/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4569bc72797f8ad9b4065c789b606b7a05e93359
--- /dev/null
+++ b/doc/university/README.md
@@ -0,0 +1,219 @@
+# GitLab University
+
+GitLab University is the best place to learn about **Version Control with Git and GitLab**.
+
+It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
+and [Blog Articles](https://about.gitlab.com/blog/).
+
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
+
+## Gitlab University Curriculum
+
+The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections.
+
+1. [GitLab Beginner](#beginner)
+1. [GitLab Intermediate](#intermediate)
+1. [GitLab Advanced](#advanced)
+1. [External Articles](#external)
+1. [Resources for GitLab Team Members](#team)
+
+---
+
+### 1. <a name="beginner"></a> GitLab Beginner
+
+#### 1.1. Version Control and Git
+
+1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
+1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
+1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
+
+#### 1.2. GitLab Basics
+
+1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
+1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
+1. [GitLab Basics - Article](../gitlab-basics/README.md)
+1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
+1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
+
+#### 1.3. Your GitLab Account
+
+1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
+
+#### 1.4. GitLab Projects
+
+1. [Repositories, Projects and Groups - Video](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Creating a Project in GitLab - Video](https://www.youtube.com/watch?v=7p0hrpNaJ14)
+1. [How to Create Files and Directories](https://about.gitlab.com/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
+1. [GitLab Todos](https://about.gitlab.com/2016/03/02/gitlab-todos-feature-highlight/)
+1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/2016/01/08/feature-highlight-wip/)
+
+#### 1.5. Migrating from other Source Control
+
+1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
+1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
+1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
+
+#### 1.6. GitLab Inc.
+
+1. [About GitLab](https://about.gitlab.com/about/)
+1. [GitLab Direction](https://about.gitlab.com/direction/)
+1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
+1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
+1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
+1. [The GitLab Book Club](bookclub/index.md)
+
+#### 1.7 Community and Support
+
+1. [Getting Help](https://about.gitlab.com/getting-help/)
+  - Proposing Features and Reporting and Tracking bugs for GitLab
+  - The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
+  - Getting Technical Support
+  - Being part of our Great Community and Contributing to GitLab
+1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
+1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
+1. [GitLab Training Workshops](https://about.gitlab.com/training)
+
+#### 1.8 GitLab Training Material
+
+1. [Git and GitLab Terminology](glossary/README.md)
+1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
+1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
+
+---
+
+### 2. <a name="intermediate"></a> GitLab Intermediate
+
+#### 2.1 GitLab Pages
+
+1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
+
+#### 2.2. GitLab Issues
+
+1. [Markdown in GitLab](../user/markdown.md)
+1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
+1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
+1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
+1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
+1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+
+#### 2.3. Continuous Integration
+
+1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
+1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
+1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [How we scale GitLab with built in Docker](https://about.gitlab.com/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
+1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+1. [Deployments and Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
+1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
+1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
+1. See **[Integrations](#integrations)** for integrations with other CI services.
+
+#### 2.4. Workflow
+
+1. [GitLab Flow - Video](https://youtu.be/enMumwvLAug?list=PLFGfElNsQthZnwMUFi6rqkyUZkI00OxIV)
+1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
+1. [GitLab Flow Documentation](https://docs.gitlab.com/ee/workflow/gitlab_flow.html)
+
+#### 2.5. GitLab Comparisons
+
+1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
+1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
+1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
+
+---
+
+### 3. <a name="advanced"></a> GitLab Advanced
+
+#### 3.1. Dev Ops
+
+1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
+1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
+1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+
+#### 3.2. Installing GitLab with Omnibus
+
+1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
+1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
+1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
+1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
+1. [Using a MySQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
+1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
+1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
+
+#### 3.3. Permissions
+
+1. [How to Manage Permissions in GitLab EE - Video](https://www.youtube.com/watch?v=DjUoIrkiNuM)
+
+#### 3.4. Large Files
+
+1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+
+#### 3.5. LDAP and Active Directory
+
+1. [How to Manage LDAP, Active Directory in GitLab - Video](https://www.youtube.com/watch?v=HPMjM-14qa8)
+
+#### 3.6 Custom Languages
+
+1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+
+#### 3.7. Scalability and High Availability
+
+1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
+1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
+
+#### 3.8 Cycle Analytics
+
+1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+
+#### 3.9. <a name="integrations"></a> Integrations
+
+1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
+1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html)
+1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
+1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
+
+---
+
+## 4. <a name="external"></a>  External Articles
+
+1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
+1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
+
+---
+
+## 5. <a name="team"></a> Resources for GitLab Team Members
+
+*Some content can only be accessed by GitLab team members*
+
+1. [Support Path](support/README.md)
+1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [User Training](training/user_training.md)
+1. [GitLab Flow Training](training/gitlab_flow.md)
+1. [Training Topics](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/topics/)
+1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
+1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
new file mode 100644
index 0000000000000000000000000000000000000000..c4229832e9fcdc990d9f889884e0a413faad0b5b
--- /dev/null
+++ b/doc/university/bookclub/booklist.md
@@ -0,0 +1,113 @@
+# Books
+
+List of books and resources, that may be worth reading.
+
+## Papers
+
+1.  **The Humble Programmer**
+
+    Edsger W. Dijkstra, 1972 ([paper](http://dl.acm.org/citation.cfm?id=361591))
+
+## Programming
+
+1.  **Design Patterns: Elements of Reusable Object-Oriented Software**
+
+    Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994 ([amazon](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612))
+
+1.  **Clean Code: A Handbook of Agile Software Craftsmanship**
+
+    Robert C. "Uncle Bob" Martin, 2008 ([amazon](http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882))
+
+1.  **Code Complete: A Practical Handbook of Software Construction**, 2nd Edition
+
+    Steve McConnell, 2004 ([amazon](http://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670))
+
+1.  **The Pragmatic Programmer: From Journeyman to Master**
+
+    Andrew Hunt, David Thomas, 1999 ([amazon](http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X))
+
+1.  **Working Effectively with Legacy Code**
+
+    Michael Feathers, 2004 ([amazon](http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052))
+
+1.  **Eloquent Ruby**
+
+    Russ Olsen, 2011 ([amazon](http://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional/dp/0321584104))
+
+1.  **Domain-Driven Design: Tackling Complexity in the Heart of Software**
+
+    Eric Evans, 2003 ([amazon](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215))
+
+1.  **How to Solve It: A New Aspect of Mathematical Method**
+
+    Polya G. 1957 ([amazon](http://www.amazon.com/How-Solve-Mathematical-Princeton-Science/dp/069116407X))
+
+1.  **Software Creativity 2.0**
+
+    Robert L. Glass, 2006 ([amazon](http://www.amazon.com/Software-Creativity-2-0-Robert-Glass/dp/0977213315))
+
+1.  **Object-Oriented Software Construction**
+
+    Bertrand Meyer, 1997 ([amazon](http://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554))
+
+1.  **Refactoring: Improving the Design of Existing Code**
+
+    Martin Fowler, Kent Beck, 1999 ([amazon](http://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672))
+
+1.  **Test Driven Development: By Example**
+
+    Kent Beck, 2002 ([amazon](http://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530))
+
+1.  **Algorithms in C++: Fundamentals, Data Structure, Sorting, Searching**
+
+    Robert Sedgewick, 1990 ([amazon](http://www.amazon.com/Algorithms-Parts-1-4-Fundamentals-Structure/dp/0201350882))
+
+1.  **Effective C++**
+
+    Scott Mayers, 1996 ([amazon](http://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876))
+
+1.  **Extreme Programming Explained: Embrace Change**
+
+    Kent Beck, 1999 ([amazon](http://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658))
+
+1.  **The Art of Computer Programming**
+
+    Donald E. Knuth, 1997 ([amazon](http://www.amazon.com/Computer-Programming-Volumes-1-4A-Boxed/dp/0321751043))
+
+1.  **Writing Efficient Programs**
+
+    Jon Louis Bentley, 1982 ([amazon](http://www.amazon.com/Writing-Efficient-Programs-Prentice-Hall-Software/dp/013970244X))
+
+1.  **The Mythical Man-Month: Essays on Software Engineering**
+
+    Frederick Phillips Brooks, 1975 ([amazon](http://www.amazon.com/Mythical-Man-Month-Essays-Software-Engineering/dp/0201006502))
+
+1.  **Peopleware: Productive Projects and Teams** 3rd Edition
+
+    Tom DeMarco, Tim Lister, 2013 ([amazon](http://www.amazon.com/Peopleware-Productive-Projects-Teams-3rd/dp/0321934113))
+
+1.  **Principles Of Software Engineering Management**
+
+    Tom Gilb, 1988 ([amazon](http://www.amazon.com/Principles-Software-Engineering-Management-Gilb/dp/0201192462))
+
+## Other
+
+1.  **Thinking, Fast and Slow**
+
+    Daniel Kahneman, 2013 ([amazon](http://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555))
+
+1.  **The Social Animal** 11th Edition
+
+    Elliot Aronson, 2011 ([amazon](http://www.amazon.com/Social-Animal-Elliot-Aronson/dp/1429233419))
+
+1.  **Influence: Science and Practice** 5th Edition
+
+    Robert B. Cialdini, 2008 ([amazon](http://www.amazon.com/Influence-Practice-Robert-B-Cialdini/dp/0205609996))
+
+1.  **Getting to Yes: Negotiating Agreement Without Giving In**
+
+    Roger Fisher, William L. Ury, Bruce Patton, 2011 ([amazon](http://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757))
+
+1.  **How to Win Friends & Influence People**
+
+    Dale Carnegie, 1981 ([amazon](http://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034))
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..022a61f44291dc68ed7292078f041c14a38d3cef
--- /dev/null
+++ b/doc/university/bookclub/index.md
@@ -0,0 +1,19 @@
+# The GitLab Book Club
+
+The Book Club is a casual meet-up to read and discuss books we like.
+We'll find a time that suits most, if not all.
+
+See the [book list](booklist.md) for additional recommendations.
+
+## Currently reading : Books about remote work
+
+1.  **Remote: Office not required**
+
+    David Heinemeier Hansson and Jason Fried, 2013
+    ([amazon](http://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
+
+1.  **The Year Without Pants**
+
+    Scott Berkun, 2013 ([ScottBerkun.com](http://scottberkun.com/yearwithoutpants/))
+
+Any other books you'd like to suggest? Edit this page and add them to the queue.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..20e7ea1987f2e7fa12638e28314031d9ef88f3b2
--- /dev/null
+++ b/doc/university/glossary/README.md
@@ -0,0 +1,589 @@
+
+## What is the Glossary
+
+This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
+Please add any terms that you discover that you think would be useful for others.
+
+### 2FA
+
+User authentication by combination of 2 different steps during login. This allows for [more security](https://about.gitlab.com/handbook/security/).
+
+### Access Levels
+
+Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../permissions/permissions.md
+
+### Active Directory (AD)
+
+A Microsoft-based [directory service](https://msdn.microsoft.com/en-us/library/bb742424.aspx) for windows domain networks. It uses LDAP technology under the hood.
+
+### Agile
+
+Building and [delivering software](http://agilemethodology.org/) in phases/parts rather than trying to build everything at once then delivering to the user/client. The latter is known as the WaterFall model.
+
+### Application Lifecycle Management (ALM)
+
+The entire product lifecycle management process for an application, from requirements management, development, and testing until deployment. GitLab has [advantages](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit#slide=id.g72f2e4906_2_288) over both legacy and modern ALM tools.
+
+### Artifactory
+
+A version control [system](https://www.jfrog.com/open-source/#os-arti) for non-text files.
+
+### Artifacts
+
+Objects (usually binary and large) created by a build process. These can include use cases, class diagrams, requirements and design documents. 
+
+### Atlassian
+
+A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. 
+
+### Audit Log
+
+Also called an [audit trail](https://en.wikipedia.org/wiki/Audit_trail), an audit log is a document that records an event in an IT system. 
+
+### Auto Defined User Group
+
+User groups are a way of centralizing control over important management tasks, particularly access control and password policies. A simple example of such groups are the users and the admins groups.
+In most of the cases these groups are auto defined in terms of access, rules of usage, conditions to be part of, etc.
+
+### Bamboo
+
+Atlassian's CI tool similar to GitLab CI and Jenkins.
+
+### Basic Subscription
+
+Entry level [subscription](https://about.gitlab.com/pricing/) for GitLab EE currently available in packs of 10.
+
+### Bitbucket
+
+Atlassian's web hosting service for Git and Mercurial Projects. Read about [migrating](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_bitbucket.html) from BitBucket to a GitLab instance. 
+
+### Branch
+
+A branch is a parallel version of a repository. This allows you to work on the repository without affecting the "master" branch, and without affecting the current "live" version. When you have made all your changes to your branch you can then merge to the master. When your merge request is accepted your changes will be "live."
+
+### Branded Login
+
+Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
+
+### Build triggers
+These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) build triggers.
+
+### CEPH
+
+ A distributed object store and file [system](http://ceph.com/) designed to provide excellent performance, reliability and scalability.
+
+### ChatOps
+
+The ability to [initiate an action](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/1412) from chat. ChatBots run in your chat application and give you the ability to do "anything" from chat. 
+
+### Clone
+
+A [copy](https://git-scm.com/docs/git-clone) of a repository stored on your machine that allows you to use your own editor without being online, but still tracks the changes made remotely.
+
+### Code Review
+
+Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab. 
+
+### Code Snippet
+
+A small amount of code, usually selected for the purpose of showing other developers how to do something specific or reproduce a problem.
+
+### Collaborator
+
+Person with read and write access to a repository who has been invited by repository owner.
+
+### Commit
+
+A [change](https://git-scm.com/docs/git-commit) (revision) to a file that also creates an ID, allowing you to see revision history and the author of the changes.
+
+### Community
+
+[Everyone](https://about.gitlab.com/community/) who uses GitLab.
+
+### Confluence
+
+Atlassian's product for collaboration on documents and projects.
+
+### Continuous Delivery
+
+A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually.
+
+### Continuous Deployment
+
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all.
+
+### Continuous Integration
+
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day.
+
+### Contributor
+
+Term used for a person contributing to an open source project.
+
+### Conversational Development (ConvDev)
+
+A [natural evolution](https://about.gitlab.com/2016/09/14/gitlab-live-event-recap/) of software development that carries a conversation across functional groups throughout the development process, enabling developers to track the full path of development in a cohesive and intuitive way. ConvDev accelerates the development lifecycle by fostering collaboration and knowledge sharing from idea to production.
+
+### Cycle Time
+
+The time it takes to move from [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab).
+
+### Data Centre
+
+Atlassian product for High Availability.
+
+### Deploy Keys
+
+A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)stored on your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
+
+### Developer
+
+For us at GitLab, this means a software developer, or someone who makes software. It is also one of the levels of access in our multi-level approval system.
+
+### DevOps 
+
+The intersection of software engineering, quality assurance, and technology operations. Explore more DevOps topics in the [glossary by XebiaLabs](https://xebialabs.com/glossary/)
+
+### Diff
+
+The difference between two commits, or saved changes. This will also be shown visually after the changes.
+
+#### Directory
+
+A folder used for storing multiple files.
+
+### Docker Container Registry
+
+A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of GitLab projects. Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+
+### Dynamic Environment
+
+### ElasticSearch
+
+Elasticsearch is a flexible, scalable and powerful search service. When [enabled](https://gitlab.com/help/integration/elasticsearch.md), it helps keep GitLab's search fast when dealing with a huge amount of data. 
+
+### Emacs
+
+### Fork
+
+Your [own copy](https://docs.gitlab.com/ce/workflow/forking_workflow.html) of a repository that allows you to make changes to the repository without affecting the original.
+
+### Gerrit
+
+A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
+
+### Git Attributes
+
+A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text file that gives attributes to pathnames. 
+
+### Git Hooks
+
+[Scripts](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) you can use to trigger actions at certain points.
+
+### GitHost.io
+
+A single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for installing, updating, hosting, and backing up customers' own private and secure GitLab instance.
+
+### GitHub
+
+A web-based Git repository hosting service with an enterprise offering. Its main features are: issue tracking, pull request with code review, abundancy of integrations and wiki. It offers free public repos, private repos and enterprise services are paid. Read about [importing a project](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_github.html) from GitHub to GitLab.
+
+### GitLab CE
+
+Our free on Premise solution with >100,000 users
+
+### GitLab CI
+
+Our own Continuos Integration [feature](https://about.gitlab.com/gitlab-ci/) that is shipped with each instance
+
+### GitLab EE
+
+Our premium on premise [solution](https://about.gitlab.com/features/#enterprise) that currently has Basic, Standard and Plus subscription packages with additional features and support.
+
+### GitLab.com
+
+Our free SaaS for public and private repositories.
+
+### GitLab Geo
+
+Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+
+### GitLab Pages
+These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account. 
+
+### Gitolite
+
+An [access layer](https://git-scm.com/book/en/v1/Git-on-the-Server-Gitolite) that sits on top of Git. Users are granted access to repos via a simple config file. As an admin, you only need the users' public SSH key and a username.
+
+### Gitorious
+
+A web-based hosting service for projects using Git. It was acquired by GitLab and we discontinued the service. Read the[Gitorious Acquisition Blog Post](https://about.gitlab.com/2015/03/03/gitlab-acquires-gitorious/).
+
+### Go
+
+An open source programming [language](https://golang.org/).
+
+### GUI/ Git GUI
+
+A portable [graphical interface](https://git-scm.com/docs/git-gui) to Git that allows users to make changes to their repository by making new commits, amending existing ones, creating branches, performing local merges, and fetching/pushing to remote repositories.
+
+### High Availability for Disaster Recovery (HADR)
+
+Sometimes written HA/DR, this usually refers to a strategy for having a failover server in place in case the main server fails.
+
+### Hip Chat
+
+Atlassian's real time chat application for teams, Hip Chat is a competitor to Slack, RocketChat and MatterMost.
+
+### High Availability
+
+Refers to a [system or component](https://about.gitlab.com/high-availability/) that is continuously operational for a desirably long length of time. Availability can be measured relative to "100% operational" or "never failing."
+
+### Inner-sourcing
+
+The [use of](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/) open source development techniques within the corporation.
+
+### Internet Relay Chat (IRC)
+
+An [application layer protocol](http://www.irchelp.org/) that facilitates communication in the form of text.
+
+### Issue Tracker
+
+A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) used to manage, organize, and maintain a list of issues, making it easier for an organization to manage.
+
+### Jenkins
+
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. 
+
+### Jira
+
+Atlassian's [project management software](https://www.atlassian.com/software/jira), i.e. a complex issue tracker. GitLab [can be configured](https://docs.gitlab.com/ee/project_services/jira.html) to interact with JIRA Core either using an on-premise instance or the SaaS solution that Atlassian offers.
+
+### JUnit
+
+A testing framework for the Java programming language, [JUnit](http://junit.org/junit4/) has been important in the evolution of test-driven development.
+
+### Kerberos
+
+A network authentication [protocol](http://web.mit.edu/kerberos/) that uses secret-key cryptography for security.
+
+### Kubernetes
+
+An open source container cluster manager originally designed by Google. It's basically a platform for automating deployment, scaling, and operations of application containers over clusters of hosts.
+
+### Labels
+
+An [identifier](https://docs.gitlab.com/ce/user/project/labels.html) to describe a group of one or more specific file revisions.
+
+### Lightweight Directory Access Protocol (LDAP)
+
+ A directory (electronic address book) with user information (e.g. name, phone_number etc.)
+
+### LDAP User Authentication
+
+GitLab [integrates](https://docs.gitlab.com/ce/administration/auth/ldap.html) with LDAP to support user authentication. This enables GitLab to sign in people from an LDAP server (i.e., allowing people whose names are on the electronic user directory server to be able to use their LDAP accounts to login.)
+
+### LDAP Group Sync
+
+Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+
+### Load Balancer
+
+A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers. 
+
+### Git Large File Storage (LFS)
+
+A way [to enable](https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/) git to handle large binary files by using reference pointers within small text files to point to the large files. Large files such as high resolution images and videos, audio files, and assets can be called from a remote server.
+
+### Linux
+
+An operating system like Windows or OS X. It is mostly used by software developers and on servers.
+
+### Markdown
+
+A lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name. Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. Checkout GitLab's [Markdown guide](https://gitlab.com/help/user/markdown.md). 
+
+### Maria DB
+
+A community developed fork/variation of MySQL. MySQL is owned by Oracle.
+
+### Master
+
+Name of the [default branch](https://git-scm.com/book/en/v1/Git-Branching-What-a-Branch-Is) in every git repository.
+
+### Mattermost
+
+An open source, self-hosted messaging alternative to Slack. View GitLab's Mattermost [feature](https://gitlab.com/gitlab-org/gitlab-mattermost). 
+
+### Mercurial
+
+A free distributed version control system similar to and a competitor with Git. 
+
+### Merge
+
+Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-merge) into another branch.
+
+### Merge Conflict
+
+[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file. 
+
+### Meteor
+
+A [platform](https://www.meteor.com) for building javascript apps.
+
+### Milestones
+
+Allow you to [organize issues](https://docs.gitlab.com/ce/workflow/milestones.html) and merge requests in GitLab 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.
+
+### Mirror Repositories
+
+A project that is setup to automatically have its branches, tags, and commits [updated from an upstream repository](https://docs.gitlab.com/ee/workflow/repository_mirroring.html). This is useful when a repository you're interested in is located on a different server, and you want to be able to browse its content and activity using the familiar GitLab interface.
+
+### MIT License
+
+A type of software license. It lets people do anything with your code with proper attribution and without warranty. It is the most common license for open source applications written in Ruby on Rails. GitLab CE is issued under this [license](https://docs.gitlab.com/ce/development/licensing.html). This means you can download the code, modify it as you want, and even build a new commercial product using the underlying code and it's not illegal. The only condition is that there is no form of warranty provided by GitLab so whatever happens when you use the code is your own problem.
+
+### Mondo Rescue
+
+A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi). 
+
+### MySQL
+
+A relational [database](http://www.mysql.com/) owned by Oracle. Currently only supported if you are using EE.
+
+### Namespace
+
+A set of symbols that are used to organize objects of various kinds so that these objects may be referred to by name. Examples of namespaces in action include file systems that assign names to files; programming languages that organize their variables and subroutines in namespaces; and computer networks and distributed systems that assign names to resources, such as computers, printers, websites, (remote) files, etc.
+
+### Nginx
+
+A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+
+### OAuth
+
+An open standard for authorization, commonly used as a way for internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password. GitLab [is](https://docs.gitlab.com/ce/integration/oauth_provider.html) an OAuth2 authentication service provider. 
+
+### Omnibus Packages
+
+A way to [package different services and tools](https://docs.gitlab.com/omnibus/) required to run GitLab, so that most developers can install it without laborious configuration.
+
+### On Premise
+
+On your own server. In GitLab, this [refers](https://about.gitlab.com/2015/02/12/why-ship-on-premises-in-the-saas-era/) to the ability to download GitLab EE/GitLab CE and host it on your own server rather than using GitLab.com, which is hosted by GitLab Inc's servers.
+
+### Open Core 
+
+GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-core-github-is-closed-source/). Coined by Andrew Lampitt in 2008, the [open core model](https://en.wikipedia.org/wiki/Open_core) primarily involves offering a "core" or feature-limited version of a software product as free and open-source software, while offering "commercial" versions or add-ons as proprietary software.
+
+### Open Source Software
+
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/). 
+
+### Owner
+
+The most powerful person on a GitLab project. They have the permissions of all the other users plus the additional permission of being able to destroy (i.e. delete) the project.
+
+### Platform as a Service (PaaS)
+
+Typically referred to in regards to application development, PaaS is a model in which a cloud provider delivers hardware and software tools to its users as a service.
+
+### Perforce
+
+The company that produces Helix.  A commercial, proprietary, centralised VCS well known for its ability to version files of any size and type.  They OEM a re-branded version of GitLab called "GitSwarm" that is tightly integrated with their "GitFusion" product, which in turn represents a portion of a Helix repository (called a depot) as a git repo.
+
+### Phabricator
+
+A suite of web-based software development collaboration tools, including the Differential code review tool, the Diffusion repository browser, the Herald change monitoring tool, the Maniphest bug tracker and the Phriction wiki. Phabricator integrates with Git, Mercurial, and Subversion.
+
+### Piwik Analytics
+
+An open source analytics software to help you analyze web traffic. It is similar to Google Analytics, except that the latter is not open source and information is stored by Google. In Piwik, the information is stored on your own server and hence is fully private.
+
+### Plus Subscription
+
+GitLab Premium EE [subscription](https://about.gitlab.com/pricing/) that includes training and dedicated Account Management and Service Engineer and complete support package.
+
+### PostgreSQL
+
+An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Touted as the most advanced open source database, it is one of two database management systems [supported by](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/database.md) GitLab, the other being MySQL. 
+
+### Protected Branches
+
+A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) that protects branches from unauthorized pushes, force pushing or deletion.
+
+### Pull
+
+Git command to [synchronize](https://git-scm.com/docs/git-pull) the local repository with the remote repository, by fetching all remote changes and merging them into the local repository.
+
+### Puppet
+
+A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
+
+### Push
+
+Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab. 
+
+### RE Read Only
+
+Permissions to see a file and its contents, but not change it.
+
+### Rebase
+
+In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another. 
+
+### (Git) Repository
+
+A directory where Git [has been initiatlized](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) to start version controlling your files. The history of your work is stored here. A remote repository is not on your machine, but usually online (like on GitLab.com, for instance). The main remote repository is usually called "Origin."
+
+### Requirements management
+
+Gives your distributed teams a single shared repository to collaborate and share requirements, understand their relationship to tests, and evaluate linked defects. It includes multiple, preconfigured requirement types.
+
+### Revision Control
+
+Also known as version control or source control, this is the management of changes to documents, computer programs, large web sites, and other collections of information. Changes are usually identified by a number or letter code, termed the "revision number," "revision level," or simply "revision."
+
+### RocketChat
+
+An open source chat application for teams, RocketChat is very similar to Slack but it is also open-source.
+
+### Route Table
+
+A route table contains rules (called routes) that determine where network traffic is directed. Each [subnet in a VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html) must be associated with a route table. 
+
+### Runners
+
+Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI.
+
+### Sidekiq
+
+The background job processor GitLab [uses](https://docs.gitlab.com/ce/administration/troubleshooting/sidekiq.html) to asynchronously run tasks.
+
+### Software as a service (SaaS)
+
+Software that is hosted centrally and accessed on-demand (i.e. whenever you want to). This applies to GitLab.com.
+
+### Software Configuration Management (SCM)
+
+This term is often used by people when they mean "Version Control."
+
+## Scrum
+
+An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
+
+### Scrum Board
+
+The board used to track the status and progress of each of the sprint backlog items.
+
+### Shell
+
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell. 
+
+### Single-tenant
+
+The tenant purchases their own copy of the software and the software can be customized to meet the specific and needs of that customer. [GitHost.io](https://about.gitlab.com/handbook/positioning-faq/) is our provider of single-tenant 'managed cloud' GitLab instances. 
+
+### Slack
+
+Real time messaging app for teams that is used internally by  GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others. 
+
+### Slave Servers
+
+Also known as secondary servers, these help to spread the load over multiple machines. They also provide backups when the master/primary server crashes.
+
+### Source Code
+
+Program code as typed by a computer programmer (i.e. it has not yet been compiled/translated by the computer to machine language).
+
+### SSH Key
+
+A unique identifier of a computer. It is used to identify computers without the need for a password (e.g., On GitLab I have [added the ssh key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html) of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.)
+
+### Single Sign On (SSO)
+
+An authentication process that allows you enter one username and password to access multiple applications.
+
+### Staging Area
+
+[Staging occurs](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics) before the commit process in git. The staging area is a file, generally contained in your Git directory, that stores information about what will go into your next commit. It’s sometimes referred to as the “index.""
+
+### Standard Subscription
+
+Our mid range EE subscription that includes 24/7 support and support for High Availability [Standard Subscription](https://about.gitlab.com/pricing/).
+
+### Stash
+
+Atlassian's Git on-premise solution. Think of it as Atlassian's GitLab EE, now known as BitBucket Server.
+
+### Static Site Generators (SSGs)
+
+A [software](https://wiki.python.org/moin/StaticSiteGenerator) that takes some text and templates as input and produces html files on the output.
+
+### Subversion
+
+Non-proprietary, centralized version control system.
+
+### Sudo
+
+A program that allows you to perform superuser/administrator actions on Unix Operating Systems (e.g., Linux, OS X.) It actually stands for 'superuser do.'
+
+### Subversion (SVN)
+
+An open source version control system. Read about [migrating from SVN](https://docs.gitlab.com/ce/workflow/importing/migrating_from_svn.html) to GitLab using SubGit. 
+
+### Tag
+
+[Represents](https://docs.gitlab.com/ce/api/tags.html) a version of a particular branch at a moment in time.
+
+### Tool Stack
+
+The set of tools used in a process to achieve a common outcome (e.g. set of tools used in Application Lifecycle Management).
+
+### Trac
+
+An open source project management and bug tracking web [application](https://trac.edgewall.org/).
+
+### Untracked files
+
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously. 
+
+### User
+
+Anyone interacting with the software.
+
+### Version Control Software (VCS)
+
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs. 
+
+### Virtual Private Cloud (VPC)
+
+An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/). 
+
+### Virtual private server (VPS)
+
+A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold as a service by an Internet hosting service. A VPS runs its own copy of an operating system, and customers have superuser-level access to that operating system instance, so they can install almost any software that runs on that OS.
+
+### VM Instance
+
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system. 
+
+### Waterfall
+
+A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the complete software to the customer that meets all the requirements they specified. 
+
+### Webhooks
+
+A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient. 
+
+### Wiki
+
+A [website/system](http://www.wiki.com/) that allows for collaborative editing of its content by the users. In programming, wikis usually contain documentation of how to use the software.
+
+### Working Tree
+
+[Consists of files](http://stackoverflow.com/questions/3689838/difference-between-head-working-tree-index-in-git) that you are currently working on.
+
+### YAML
+
+A human-readable data serialization [language](http://www.yaml.org/about.html) that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail. 
+
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..088f1cd72903ef627f630d6041b645fab9de0f75
--- /dev/null
+++ b/doc/university/high-availability/aws/README.md
@@ -0,0 +1,387 @@
+
+# High Availability on AWS
+
+GitLab on AWS can leverage many of the services that are already
+configurable with High Availability. These services have a lot of
+flexibility and are able to adopt to most companies, best of all is the
+ability to automate both vertical and horizontal scaling.
+
+In this article we'll go through a basic HA setup where we'll start by
+configuring our Virtual Private Cloud and subnets to later integrate
+services such as RDS for our database server and ElastiCache as a Redis
+cluster to finally manage them within an auto scaling group with custom
+scaling policies.
+
+***
+
+## Where to Start
+
+Login to your AWS account through the `My Account` dropdown on
+`https://aws.amazon.com` or through the URI assigned to your team such as
+`https://myteam.signin.aws.amazon.com/console/`. You'll start on the
+Amazon Web Services console from where we can choose all of the services
+we'll be using to configure our cloud infrastructure.
+
+***
+
+## Network
+
+We'll start by creating a VPC for our GitLab cloud infrastructure, then
+we can create subnets to have public and private instances in at least
+two AZs. Public subnets will require a Route Table keep an associated
+Internet Gateway.
+
+### VPC
+
+Start by looking for the VPC option on the web console. Now create a new
+VPC. We can use `10.0.0.0/16` for the CIDR block and leave tenancy as
+default if we don't require dedicated hardware.
+
+![New VPC](img/new_vpc.png)
+
+If you're setting up the Elastic File System service then select the VPC
+and from the Actions dropdown choose Edit DNS Hostnames and select Yes.
+
+### Subnet
+
+Now let's create some subnets in different Availability Zones. Make sure
+that each subnet is associated the the VPC we just created, that it has
+a distinct VPC and lastly that CIDR blocks don't overlap. This will also
+allow us to enable multi AZ for redundancy.
+
+We will create private and public subnets to match load balancers and
+RDS instances as well.
+
+![Subnet Creation](img/subnet.png)
+
+The subnets are listed with their name, AZ and CIDR block:
+
+* gitlab-public-10.0.0.0  - us-west-2a - 10.0.0.0
+* gitlab-private-10.0.1.0 - us-west-2a - 10.0.1.0
+* gitlab-public-10.0.2.0  - us-west-2b - 10.0.2.0
+* gitlab-private-10.0.3.0 - us-west-2b - 10.0.3.0
+
+### Route Table
+
+Up to now all our subnets are private. We need to create a Route Table
+to associate an Internet Gateway. On the same VPC dashboard choose
+Route Tables on the left column and give it a name and associate it to
+our newly created VPC.
+
+![Route Table](img/route_table.png)
+
+
+### Internet Gateway
+
+Now still on the same dashboard head over to Internet Gateways and
+create a new one. After its created pres on the `Attach to VPC` button and
+select our VPC.
+
+![Internet Gateway](img/ig.png)
+
+### Configure Subnets
+
+Go back to the Router Tables screen and select the newly created one,
+press the Routes tab on the bottom section and edit it. We need to add a
+new target which will be our Internet Gateway and have it receive
+traffic from any destination.
+
+![Subnet Config](img/ig-rt.png)
+
+Before leaving this screen select the next tab to the rgiht which is
+Subnet Associations and add our public subnets. If you followed our
+naming convention they should be easy to find.
+
+***
+
+## Database with RDS
+
+For our database server we will use Amazon RDS which offers Multi AZ
+for redundancy. Lets start by creating a subnet group and then we'll
+create the actual RDS instance.
+
+### Subnet Group
+
+From the RDS dashboard select Subnet Groups. Lets select our VPC from
+the VPC ID dropdown and at the bottom we can add our private subnets.
+
+![Subnet Group](img/db-subnet-group.png)
+
+### RDS
+
+Select the RDS service from the Database section and create a new
+PostgreSQL instance. After choosing between a Production or
+Development instance we'll start with the actual configuration. On the
+image bellow we have the settings for this article but note the
+following two options which are of particular interest for HA:
+
+1. Multi-AZ-Deployment is recommended as redundancy. Read more at
+[High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html)
+1. While we chose a General Purpose (SSD) for this article a Provisioned
+IOPS (SSD) is best suited for HA. Read more about it at
+[Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
+
+![RDS Instance Specs](img/instance_specs.png)
+
+The rest of the setting on this page request a DB identifier, username
+and a master password. We've chosen to use `gitlab-ha`, `gitlab` and a
+very secure password respectively. Keep these in hand for later.
+
+![Network and Security](img/rds-net-opt.png)
+
+Make sure to choose our gitlab VPC, our subnet group, not have it public,
+and to leave it to create a new security group. The only additional
+change which will be helpful is the database name for which we can use
+`gitlabhq_production`.
+
+***
+
+## ElastiCache
+
+EC is an in-memory hosted caching solution. Redis maintains its own
+persistance and is used for certain types of application.
+
+Let's choose the ElastiCache service in the Database section from our
+AWS console. Now lets create a cache subnet group which will be very
+similar to the RDS subnet group. Make sure to select our VPC and its
+private subnets.
+
+![ElastiCache](img/ec-subnet.png)
+
+Now press the Launch a Cache Cluster and choose Redis for our
+DB engine. You'll be able to configure details such as replication,
+Multi AZ and node types. The second section will allow us to choose our
+subnet and security group and     
+
+![Redis Cluster details](img/redis-cluster-det.png)
+
+![Redis Network](img/redis-net.png)
+
+***
+
+## Elastic File System
+
+This new AWS offering allows us to create a file system accessible by

+EC2 instances within a VPC. Choose our VPC and the subnets will be
+
automatically configured assuming we don't need to set explicit IPs.
+The
next section allows us to add tags and choose between General
+Purpose or
Max I/O which is a good option when being accessed by a
+large number of
EC2 instances.
+
+

![Elastic File System](img/elastic-file-system.png)
+
+To actually mount and install the NFS client we'll use the User Data
+section when adding our Launch Configuration.
+
+***
+
+## Initiate AMI
+
+We are going to launch an EC2 instance and bake an image so that we can
+later use it for auto scaling. We'll also take this opportunity to add an
+extension to our RDS through this temporary EC2 instance.
+
+### EC2 Instance
+
+Look for the EC2 option and choose to create an instance. We'll need at
+least a t2.medium type and for this article we'll choose an Ubuntu 14.04
+HVM 64-bit. In the Configure Instance section choose our GitLab VPC and
+a public subnet. I'd choose at least 10GB of storage.
+
+In the security group we'll create a new one considering that we need to
+SSH into the instance and also try it out through http. So let's add the
+http traffic from anywhere and name it something such as
+`gitlab-ec2-security-group`.
+
+While we wait for it to launch we can allocate an Elastic IP and
+associate it with our new EC2 instance.  
+
+### RDS and Redis Security Group
+
+After the instance is being created we will navigate to our EC2 security
+groups and add a small change for our EC2 instances to be able to
+connect to RDS. First copy the security group name we just defined,
+namely `gitlab-ec2-security-group`, and edit select the RDS security
+group and edit the inbound rules. Choose the rule type to be PostgreSQL
+and paste the name under source.
+
+![RDS security group](img/rds-sec-group.png)
+
+Similar to the above we'll jump to the `gitlab-ec2-security-group` group
+and add a custom TCP rule for port 6379 accessible within itself.
+
+### Install GitLab
+
+To connect through SSH you will need to have the `pem` file which you
+chose available and with the correct permissions such as `400`.
+
+After accessing your server don't forget to update and upgrade your
+packages.
+
+    sudo apt-get update && sudo apt-get upgrade -y
+
+Then follow installation instructions from
+[GitLab](https://about.gitlab.com/downloads-ee/#ubuntu1404), but before
+running reconfigure we need to make sure all our services are tied down
+so just leave the reconfigure command until after we edit our gitlab.rb
+file.
+
+
+### Extension for PostgreSQL
+
+Connect to your new RDS instance to verify access and to install
+a required extension. We can find the host or endpoint by selecting the
+instance and  we just created and after the details drop down we'll find
+it labeled as 'Endpoint'; do remember not to include the colon and port
+number.
+
+    sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+    psql (9.4.7)
+    Type "help" for help.
+
+    gitlab=# CREATE EXTENSION pg_trgm;
+    gitlab=# \q
+
+### Configure GitLab
+
+While connected to your server edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb`
+find the `external_url 'http://gitlab.example.com'` option and change it
+to the domain you will be using or the public IP address of the current
+instance to test the configuration.
+
+For a more detailed description about configuring GitLab read [Configuring GitLab for HA](http://docs.gitlab.com/ee/administration/high_availability/gitlab.html)
+
+Now look for the GitLab database settings and uncomment as necessary. In
+our current case we'll specify the adapter, encoding, host, db name,
+username, and password.
+
+    gitlab_rails['db_adapter'] = "postgresql"
+    gitlab_rails['db_encoding'] = "unicode"    
+    gitlab_rails['db_database'] = "gitlabhq_production"   
+    gitlab_rails['db_username'] = "gitlab"
+    gitlab_rails['db_password'] = "mypassword"
+    gitlab_rails['db_host'] = "<rds-endpoint>"
+
+Next we only need to configure the Redis section by adding the host and
+uncommenting the port.
+
+
+
+The last configuration step is to [change the default file locations ](http://docs.gitlab.com/ee/administration/high_availability/nfs.html)
+to make the EFS integration easier to manage.
+
+    gitlab_rails['redis_host'] = "<redis-endpoint>"
+    gitlab_rails['redis_port'] = 6379
+
+Finally run reconfigure, you might find it useful to run a check and
+a service status to make sure everything has been setup correctly.
+
+    sudo gitlab-ctl reconfigure  
+    sudo gitlab-rake gitlab:check  
+    sudo gitlab-ctl status  
+
+If everything looks good copy the Elastic IP over to your browser and
+test the instance manually.
+
+### AMI
+
+After you finish testing your EC2 instance go back to its dashboard and
+while the instance is selected press on the Actions dropdown to choose
+Image -> Create an Image. Give it a name and description and confirm.
+
+***
+
+## Load Balancer
+
+On the same dashboard look for Load Balancer on the left column and press
+the Create button. Choose a classic Load Balancer, our gitlab VPC, not
+internal and make sure its listening for HTTP and HTTPS on port 80.
+
+Here is a tricky part though, when adding subnets we need to associate
+public subnets instead of the private ones where our instances will
+actually live.
+
+On the secruity group section let's create a new one named
+`gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic
+from anywhere.
+
+The Load Balancer Health will allow us to indicate where to ping and what
+makes up a healthy or unhealthy instance.
+
+We won't add the instance on the next session because we'll destroy it
+momentarily as we'll be using the image we where creating. We will keep
+the Enable Cross-Zone and Enable Connection Draining active.
+
+After we finish creating the Load Balancer we can re visit our Security
+Groups to improve access only through the ELB and any other requirement
+you might have.
+
+***
+
+## Auto Scaling Group
+
+Our AMI should be done by now so we can start working on our Auto
+Scaling Group.
+
+This option is also available through the EC2 dashboard on the left
+sidebar. Press on the create button. Select the new image on My AMIs and
+give it a `t2.medium` size. To be able to use Elastic File System we need
+to add a script to mount EFS automatically at launch. We'll do this at
+the Advanced Details section where we have a [User Data](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html)
+text area that allows us to add a lot of custom configurations which
+allows you to add a custom script for when launching an instance. Let's
+add the following script to the User Data section:
+
+
+    #cloud-config
+    package_upgrade: true
+    packages:
+    - nfs-common
+    runcmd:
+    - mkdir -p /gitlab-data
+    - chown ec2-user:ec2-user /gitlab-data
+    - echo "$(curl --silent http://169.254.169.254/latest/meta-data/placement/availability-zone).file-system-id.aws-region.amazonaws.com:/ /gitlab-data nfs defaults,vers=4.1 0 0" >> /etc/fstab
+    - mount -a -t nfs
+    - sudo gitlab-ctl reconfigure
+
+On the security group section we can chosse our existing
+`gitlab-ec2-security-group` group which has already been tested.
+
+After this is launched we are able to start creating our Auto Scaling
+Group. Start by giving it a name and assinging it our VPC and private
+subnets. We also want to always start with two instances and if you
+scroll down to Advanced Details we can choose to receive traffic from ELBs.
+Lets enable that option and select our ELB. We also want to use the ELB's
+health check.
+
+![Auto scaling](img/auto-scaling-det.png)
+
+### Policies
+
+This is the really great part of Auto Scaling, we get to choose when AWS
+launches new instances and when it removes them. For this group we'll
+scale between 2 and 4 instances where one instance will be added if CPU
+utilization is greater than 60% and one instance is removed if it falls
+to less than 45%. Here are the complete policies:
+
+![Policies](img/policies.png)
+
+You'll notice that after we save this AWS starts launching our two
+instances in different AZs and without a public IP which is exactly what
+we where aiming for.
+
+***
+
+## Final Thoughts
+
+After you're done with the policies section have some fun trying to break
+instances. You should be able to see how the Auto Scaling Group and the
+EC2 screen start bringing them up again.
+
+High Availability is a very big area, we went mostly through scaling and
+some redundancy options but it might also imply Geographic replication.
+There is a lot of ground yet to cover so have a read through these other
+resources and feel free to open an issue to request additional material.
+
+ * [GitLab High Availability](http://docs.gitlab.com/ce/administration/high_availability/README.html#sts=High Availability)
+ * [GitLab Geo](http://docs.gitlab.com/ee/gitlab-geo/README.html)  
diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png
new file mode 100644
index 0000000000000000000000000000000000000000..e9b655294953ce75fc39f7ff4a9caf064daf32bd
Binary files /dev/null and b/doc/university/high-availability/aws/img/auto-scaling-det.png differ
diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png
new file mode 100644
index 0000000000000000000000000000000000000000..0768aa73c45e437bd3fb75377ab5075eb944659f
Binary files /dev/null and b/doc/university/high-availability/aws/img/db-subnet-group.png differ
diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png
new file mode 100644
index 0000000000000000000000000000000000000000..f41d78b271d9d7e9d09fab2a9fae2e0f6d02cff6
Binary files /dev/null and b/doc/university/high-availability/aws/img/ec-subnet.png differ
diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png
new file mode 100644
index 0000000000000000000000000000000000000000..7de866d1e89f441fd9966716b6b2b592de4c5eda
Binary files /dev/null and b/doc/university/high-availability/aws/img/elastic-file-system.png differ
diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png
new file mode 100644
index 0000000000000000000000000000000000000000..93bb0c2ae023391e9a8ad6b52a7cee4719e4dbd3
Binary files /dev/null and b/doc/university/high-availability/aws/img/ig-rt.png differ
diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc50456370f7d4614a50dbe56f97c11a5b887cba
Binary files /dev/null and b/doc/university/high-availability/aws/img/ig.png differ
diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef31dc41dae7afb0d094c8ccd62f77e523b43914
Binary files /dev/null and b/doc/university/high-availability/aws/img/instance_specs.png differ
diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png
new file mode 100644
index 0000000000000000000000000000000000000000..4aac6af7c7a7bc87c23efb4c26520edc99b4adf5
Binary files /dev/null and b/doc/university/high-availability/aws/img/new_vpc.png differ
diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png
new file mode 100644
index 0000000000000000000000000000000000000000..8c58117e4fab7f1526555b2141ccf3078409a133
Binary files /dev/null and b/doc/university/high-availability/aws/img/policies.png differ
diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc204de2474467a0594ecec698ca7fc2402bd734
Binary files /dev/null and b/doc/university/high-availability/aws/img/rds-net-opt.png differ
diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png
new file mode 100644
index 0000000000000000000000000000000000000000..8864dc3e4636051287a0e900c966fd92cd2a0a23
Binary files /dev/null and b/doc/university/high-availability/aws/img/rds-sec-group.png differ
diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e9a81283c5f1ef4076a059ec53bd7e1d74aa386
Binary files /dev/null and b/doc/university/high-availability/aws/img/redis-cluster-det.png differ
diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png
new file mode 100644
index 0000000000000000000000000000000000000000..037bd6d68973c3b3a0d274b8d3474ee77ec26797
Binary files /dev/null and b/doc/university/high-availability/aws/img/redis-net.png differ
diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png
new file mode 100644
index 0000000000000000000000000000000000000000..1dea322474deaf778e6ff5a4117197066a4c5341
Binary files /dev/null and b/doc/university/high-availability/aws/img/route_table.png differ
diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png
new file mode 100644
index 0000000000000000000000000000000000000000..dbc712019923d73942fc09b71147d84f89ee28be
Binary files /dev/null and b/doc/university/high-availability/aws/img/subnet.png differ
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7ff53c2cc3f8c33cb2ea1c5fb4f73c5685918f5c
--- /dev/null
+++ b/doc/university/process/README.md
@@ -0,0 +1,30 @@
+---
+title: University | Process
+---
+
+## Suggesting improvements
+
+If you would like to teach a class or participate or help in any way please
+submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
+
+If you have suggestions for additional courses you would like to see,
+please submit a merge request to add an upcoming class, assign to
+[Chad](https://gitlab.com/u/chadmalchow) and /cc [Job](https://gitlab.com/u/JobV).
+
+## Adding classes
+
+1. All training materials of any kind should be added to [GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/)
+   to ensure they are available to a broad audience (don't use any other repo or
+   storage for training materials).
+1. Don't make materials that are needlessly specific to one group of people, try
+   to keep the wording broad and inclusive (don't make things for only GitLab Inc.
+   people, only interns, only customers, etc.).
+1. To allow people to contribute all content should be in git.
+1. The content can go in a subdirectory under `/doc/university/`.
+1. To make, view or modify the slides of the classes use [Deckset](http://www.decksetapp.com/)
+   or [RevealJS](http://lab.hakim.se/reveal-js/). Do not use Powerpoint or Google
+   Slides since this prevents everyone from contributing.
+1. Please upload any video recordings to our Youtube channel. We prefer them to
+   be public, if needed they can be unlisted but if so they should be linked from
+   this page.
+1. Please create a merge request and assign to [SeanPackham](https://gitlab.com/u/SeanPackham).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6e415e4d219250a10a1982427c35779833bd0f8f
--- /dev/null
+++ b/doc/university/support/README.md
@@ -0,0 +1,188 @@
+
+## Support Boot Camp
+
+**Goal:** Prepare new Service Engineers at GitLab
+
+For each stage there are learning goals and content to support the learning of the engineer.
+The goal of this boot camp is to have every Service Engineer prepared to help our customers
+with whatever needs they might have and to also assist our awesome community with their
+questions.
+
+Always start with the [University Overview](../README.md) and then work
+your way here for more advanced and specific training. Once you feel comfortable
+with the topics of the current stage, move to the next.
+
+### Stage 1
+
+Follow the topics on the [University Overview](../README.md), concentrate on it
+during your first Stage, but also:
+
+- Perform the [first steps](https://about.gitlab.com/handbook/support/onboarding/#first-steps) of
+   the on-boarding process for new Service Engineers
+
+#### Goals
+
+Aim to have a good overview of the Product and main features, Git and the Company
+
+### Stage 2
+
+Continue to look over remaining portions of the [University Overview](../README.md) and continue on to these topics:
+
+#### Set up your development machine
+
+Get your development machine ready to familiarize yourself with the codebase, the components, and to be prepared to reproduce issues that our users encounter
+
+- Install the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit)
+  - [Setup OpenLDAP as part of this](https://gitlab.com/gitlab-org/gitlab-development-kit#openldap)
+
+#### Become comfortable with the Installation processes that we support
+
+It's important to understand how to install GitLab in the same way that our users do. Try installing different versions and upgrading and downgrading between them. Installation from source will give you a greater understanding of the components that we employ and how everything fits together.
+
+Sometimes we need to upgrade customers from old versions of GitLab to latest, so it's good to get some experience of doing that now.
+
+- [Installation Methods](https://about.gitlab.com/installation/):
+  - [Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/)
+  - [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
+  - [Source](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md)
+- Get yourself a Digital Ocean droplet, where you can install and maintain your own instance of GitLab
+  - Ask in #infrastructure about this
+  - Populate with some test data
+  - Keep this up-to-date as patch and version releases become available, just like our customers would
+- Try out the following installation path
+  - [Install GitLab 4.2 from source](https://gitlab.com/gitlab-org/gitlab-ce/blob/d67117b5a185cfb15a1d7e749588ff981ffbf779/doc/install/installation.md)
+    - External MySQL database
+    - External NGINX
+  - Create some test data
+    - Populated Repos
+    - Users
+    - Groups
+    - Projects
+  - [Backup using our Backup rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+  - [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/4.2-to-5.0.md)
+  - [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.0-to-5.1.md)
+  - [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.1-to-6.0.md)
+  - [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/6.x-or-7.x-to-7.14.md)
+  - [Backup using our Backup rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+  - [Perform the MySQL to PostgreSQL migration to convert your backup](https://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres)
+  - [Upgrade to Omnibus 7.14](https://docs.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
+  - [Restore backup using our Restore rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup)
+  - [Upgrade to latest EE](https://about.gitlab.com/downloads-ee)
+    - (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
+- Perform a downgrade from [EE to CE](https://docs.gitlab.com/ee/downgrade_ee_to_ce/README.html)
+
+#### Start to learn about some of the integrations that we support
+
+Our integrations add great value to GitLab. User questions often relate to integrating GitLab with existing external services and the configuration involved
+
+- Learn about our Integrations (specially, not only):
+  - [LDAP](https://docs.gitlab.com/ee/integration/ldap.html)
+  - [JIRA](https://docs.gitlab.com/ee/project_services/jira.html)
+  - [Jenkins](https://docs.gitlab.com/ee/integration/jenkins.html)
+  - [SAML](https://docs.gitlab.com/ce/integration/saml.html)
+
+#### Goals
+
+- Aim to be comfortable with installation of the GitLab product and configuration of some of the major integrations
+- Aim to have an installation available for reproducing customer reports
+
+### Stage 3
+
+#### Understand the gathering of diagnostics for GitLab instances
+
+- Learn about the GitLab checks that are available
+  - [Environment Information and maintenance checks](https://docs.gitlab.com/ce/raketasks/maintenance.html)
+  - [GitLab check](https://docs.gitlab.com/ce/raketasks/check.html)
+  - Omnibus commands
+    - [Status](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#get-service-status)
+    - [Starting and stopping services](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#starting-and-stopping)
+    - [Starting a rails console](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#invoking-rake-tasks)
+
+#### Learn about the Support process
+
+Zendesk is our Support Centre and our main communication line with our Customers. We communicate with customers through several other channels too
+
+- Familiarize yourself with ZenDesk
+  - [UI Overview](https://support.zendesk.com/hc/en-us/articles/203661806-Introduction-to-the-Zendesk-agent-interface)
+  - [Updating Tickets](https://support.zendesk.com/hc/en-us/articles/212530318-Updating-and-solving-tickets)
+  - [Working w/ Tickets](https://support.zendesk.com/hc/en-us/articles/203690856-Working-with-tickets) *Read: avoiding agent collision.*
+- Dive into our ZenDesk support process by reading how to [handle tickets](https://about.gitlab.com/handbook/support/onboarding/#handling-tickets)
+- Start getting real world experience by handling real tickets, all the while gaining further experience with the Product.
+  - First, learn about our [Support Channels](https://about.gitlab.com/handbook/support/#support-channels)
+  - Ask other Service Engineers for help, when necessary, and to review your responses
+  - Start with [StackOverflow](https://about.gitlab.com/handbook/support/#stack-overflowa-namestack-overflowa) and the [GitLab forum](https://about.gitlab.com/handbook/support/#foruma-namegitlab-foruma)
+  - Here you will find a large variety of queries mainly from our Users who are self hosting GitLab CE
+  - Understand the questions that are asked and dig in to try to find a solution
+  - [Proceed on to the GitLab.com Support Forum](https://about.gitlab.com/handbook/support/#gitlabcom-support-trackera-namesupp-foruma)
+    - Here you will find queries regarding our own GitLab.com
+    - Helping Users here will give you an understanding of our Admin interface and other tools
+  - [Proceed on to the Twitter tickets in Zendesk](https://about.gitlab.com/handbook/support/#twitter)
+    - Here you will gain a great insight into our userbase
+    - Learn from any complaints and problems and feed them back to the team
+    - Tweets can range from help needed with GitLab installations, the API and just general queries
+  - [Proceed on to Regular email Support tickets](https://about.gitlab.com/handbook/support/#regular-zendesk-tickets-a-nameregulara)
+    - Here you will find tickets from our GitLab EE Customers and GitLab CE Users
+    - Tickets here are extremely varied and often very technical
+    - You should be prepared for these tickets, given the knowledge gained from previous tiers and your training
+- Check out your colleagues' responses
+  - Hop on to the #support-live-feed channel in Slack and see the tickets as they come in and are updated
+  - Read through old tickets that your colleagues have worked on
+- Start arranging to pair on calls with other Service Engineers. Aim to cover a few of each type of call
+  - [Learn about Cisco WebEx](https://about.gitlab.com/handbook/support/onboarding/#webexa-namewebexa)
+  - Training calls
+  - Information gathering calls
+    - It's good to find out how new and prospective customers are going to be using the product and how they will set up their infrastructure
+  - Diagnosis calls
+    - When email isn't enough we may need to hop on a call and do some debugging along side the customer
+    - These paired calls are a great learning experience
+  - Upgrade calls
+  - Emergency calls
+
+#### Learn about the Escalation process for tickets
+
+Some tickets need specific knowledge or a deep understanding of a particular component and will need to be escalated to a Senior Service Engineer or Developer
+
+- Read about [Escalation](https://about.gitlab.com/handbook/support/onboarding/#create-issuesa-namecreate-issuea)
+- Find the macros in Zendesk for ticket escalations
+- Take a look at the [GitLab.com Team page](https://about.gitlab.com/team/) to find the resident experts in their fields
+
+#### Learn about raising issues and fielding feature proposals
+
+- Understand what's in the pipeline and proposed features at GitLab: [Direction Page](https://about.gitlab.com/direction/)
+- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab-ce/labels) to find existing feature proposals and bugs
+- If raising a new issue always provide a relevant label and a link to the relevant ticket in Zendesk
+- Add [customer labels](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=customer) for those issues relevant to our subscribers
+- Take a look at the [existing issue templates](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker) to see what is expected
+- Raise issues for bugs in a manner that would make the issue easily reproducible. A Developer or a contributor may work on your issue
+
+#### Goals
+
+- Aim to have a good understanding of the problems that customers are facing
+- Aim to have gained experience in scheduling and participating in calls with customers
+- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels
+
+### Stage 4
+
+#### Advanced GitLab topics
+
+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)
+- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
+- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
+- Get to know the GitLab Codebase by reading through the source code:
+  - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+     and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+- Ask as many questions as you can think of on the `#support` chat channel
+
+#### Get initiated for on-call duty
+
+- Read over the [public run-books to understand common tasks](https://gitlab.com/gitlab-com/runbooks)
+- Create an issue on the internal Organization tracker to schedule time with the DevOps / Production team, so that you learn how to handle GitLab.com going down. Once you are trained for this, you are ready to be added to the on-call rotation.
+
+#### Goals
+
+- Aim to become a fully-fledged Service Engineer!
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..03c62a81b102d812ad46c3bca482a5cd57e5f023
--- /dev/null
+++ b/doc/university/training/end-user/README.md
@@ -0,0 +1,420 @@
+
+# Training
+
+This training material is the markdown used to generate training slides
+which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-training-slides/#/)
+through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides)
+project.
+
+---
+
+## Git Intro
+
+---
+
+### What is a Version Control System (VCS)
+
+- Records changes to a file
+- Maintains history of changes
+- Disaster Recovery
+- Types of VCS: Local, Centralized and Distributed
+
+---
+
+### Short Story of Git
+
+- 1991-2002: The Linux kernel was being maintaned by sharing archived files
+  and patches.
+- 2002: The Linux kernel project began using a DVCS called BitKeeper
+- 2005: BitKeeper revoked the free-of-charge status and Git was created
+
+---
+
+### What is Git
+
+- Distributed Version Control System
+- Great branching model that adapts well to most workflows
+- Fast and reliable
+- Keeps a complete history
+- Disaster recovery friendly
+- Open Source
+
+---
+
+### Getting Help
+
+- Use the tools at your disposal when you get stuck.
+  - Use `git help <command>` command
+  - Use Google (i.e. StackOverflow, Google groups)
+  - Read documentation at https://git-scm.com
+
+---
+
+## Git Setup
+Workshop Time!
+
+---
+
+### Setup
+
+- Windows: Install 'Git for Windows'
+  - https://git-for-windows.github.io
+- Mac: Type `git` in the Terminal application.
+  - If it's not installed, it will prompt you to install it.
+- Linux
+  - Debian: `sudo apt-get install git-all`
+  - Red Hat `sudo yum install git-all`
+
+---
+
+### Configure
+
+- One-time configuration of the Git client:
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```    
+
+- If you don't use the global flag you can setup a different author for
+  each project
+- Check settings with:
+
+```bash
+git config --global --list
+```
+- You might want or be required to use an SSH key.
+    - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html)
+
+---
+
+### Workspace
+
+- Choose a directory on you machine easy to access
+- Create a workspace or development directory
+- This is where we'll be working and adding content
+
+---
+
+```bash
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace  
+```
+
+---
+
+## Git Basics
+
+---  
+
+### Git Workflow
+
+- Untracked files
+    - New files that Git has not been told to track previously.
+- Working area (Workspace)
+    - Files that have been modified but are not committed.
+- Staging area (Index)
+    - Modified files that have been marked to go in the next commit.
+- Upstream
+    - Hosted repository on a shared server
+
+---
+
+### GitLab
+
+- GitLab is an application to code, test and deploy.
+- Provides repository management with access controls, code reviews,
+  issue tracking, Merge Requests, and other features.
+- The hosted version of GitLab is gitlab.com
+
+---  
+
+### New Project
+
+- Sign in into your gitlab.com account
+- Create a project
+- Choose to import from 'Any Repo by URL' and use https://gitlab.com/gitlab-org/training-examples.git
+- On your machine clone the `training-examples` project
+
+---
+
+### Git and GitLab basics
+
+1. Edit `edit_this_file.rb` in `training-examples`
+2. See it listed as a changed file (working area)
+3. View the differences
+4. Stage the file
+5. Commit
+6. Push the commit to the remote
+7. View the git log
+
+---
+
+```shell
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---  
+
+### Feature Branching
+
+1. Create a new feature branch called `squash_some_bugs`
+2. Edit `bugs.rb` and remove all the bugs.
+3. Commit
+4. Push
+
+---
+
+```shell
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+## Merge Request
+
+---
+
+### Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add `WIP` to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+
+---
+
+### Merge request example
+
+- Create your first merge request
+  - Use the blue button in the activity feed
+  - View the diff (changes) and leave a comment
+  - Push a new commit to the same branch
+  - Review the changes again and notice the update
+
+---
+
+### Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+- Feedback and Collaboration
+
+---
+
+### Feedback and Collaboration
+
+- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review)
+- See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+## Merge Conflicts
+
+---
+
+### Merge Conflicts
+* Happen often
+* Learning to fix conflicts is hard
+* Practice makes perfect
+* Force push after fixing conflicts. Be careful!
+
+---
+
+### Example Plan
+1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the conflicts.rb file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+---
+
+### Example 1/2
+
+    git checkout -b conflicts_branch
+
+    # vi conflicts.rb
+    # Add 'Line4' and 'Line5'
+
+    git commit -am "add line4 and line5"
+    git push origin conflicts_branch
+
+    git checkout master
+
+    # vi conflicts.rb
+    # Add 'Line6' and 'Line7'
+    git commit -am "add line6 and line7"
+    git push origin master
+
+---
+
+### Example 2/2
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+    git checkout conflicts_branch
+    git fetch
+    git rebase master
+
+    # Fix conflicts by editing the files.
+
+    git add conflicts.rb
+    # No need to commit this file
+
+    git rebase --continue
+
+    # Remember that we have rewritten our commit history so we
+    # need to force push so that our remote branch is restructured
+    git push origin conflicts_branch -f
+
+---
+
+### Notes
+
+* When to use `git merge` and when to use `git rebase`
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
+
+---
+
+## Revert and Unstage
+
+---
+
+### Unstage
+
+To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch:
+
+    git reset HEAD <file>
+
+This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+    git checkout -- <file>
+
+To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag:
+
+    git rm '*.txt'
+    git rm -r <dirname>
+
+If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our .gitignore file then use `--cache`:
+
+    git rm <filename> --cache
+
+---
+
+### Undo Commits
+
+Undo last commit putting everything back into the staging area:
+
+    git reset --soft HEAD^
+
+Add files and change message with:
+
+    git commit --amend -m "New Message"
+
+Undo last and remove changes
+
+    git reset --hard HEAD^
+
+Same as last one but for two commits back:
+
+    git reset --hard HEAD^^
+
+Don't reset after pushing
+
+---
+
+### Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+----
+
+    # Change file edit_this_file.rb
+    git status
+    git commit -am "kjkfjkg"
+    git log
+    git commit --amend -m "New comment added"
+    git log
+    git reset --soft HEAD^
+    git log
+    git pull origin master
+    git push origin master
+
+---
+
+### Note
+
+git revert vs git reset  
+Reset removes the commit while revert removes the changes but leaves the commit  
+Revert is safer considering we can revert a revert  
+
+
+    # Changed file
+    git commit -am "bug introduced"
+    git revert HEAD
+    # New commit created reverting changes
+    # Now we want to re apply the reverted commit
+    git log # take hash from the revert commit
+    git revert <rev commit hash>
+    # reverted commit is back (new commit created again)
+
+---
+
+## Questions
+
+---
+
+## Instructor Notes
+
+---
+
+### Version Control
+ - Local VCS was used with a filesystem or a simple db.
+ - Centralized VCS such as Subversion includes collaboration but
+   still is prone to data loss as the main server is the single point of
+   failure.
+ - Distributed VCS enables the team to have a complete copy of the project
+   and work with little dependency to the main server. In case of a main
+   server failing the project can be recovered by any of the latest copies
+   from the team
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
new file mode 100755
index 0000000000000000000000000000000000000000..a7db1f2e06996bc70239a8e1fdacd21ed66caf42
--- /dev/null
+++ b/doc/university/training/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+---
+
+# Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](gitlab_flow/feature_branches.png)
+
+---
+
+# Production branch
+
+- One, long-running production release branch
+  as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+---
+
+# Production branch
+
+![inline](gitlab_flow/production_branch.png)
+
+---
+
+# Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+  from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+---
+
+# Release branch
+
+![inline](gitlab_flow/release_branches.png)
+
+---
+
+# More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/gitlab_flow/feature_branches.png b/doc/university/training/gitlab_flow/feature_branches.png
new file mode 100644
index 0000000000000000000000000000000000000000..88addb623ee0e554345dfbbde879e464befae85d
Binary files /dev/null and b/doc/university/training/gitlab_flow/feature_branches.png differ
diff --git a/doc/university/training/gitlab_flow/production_branch.png b/doc/university/training/gitlab_flow/production_branch.png
new file mode 100644
index 0000000000000000000000000000000000000000..33fb26dd62163cc5d9436a91db508971e73fc1f5
Binary files /dev/null and b/doc/university/training/gitlab_flow/production_branch.png differ
diff --git a/doc/university/training/gitlab_flow/release_branches.png b/doc/university/training/gitlab_flow/release_branches.png
new file mode 100644
index 0000000000000000000000000000000000000000..da7ae53413a77246118b2cbb320b5707d4ee965e
Binary files /dev/null and b/doc/university/training/gitlab_flow/release_branches.png differ
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
new file mode 100755
index 0000000000000000000000000000000000000000..03179ff5a777fda7223836900c46a9931cb3db9c
--- /dev/null
+++ b/doc/university/training/index.md
@@ -0,0 +1,6 @@
+# GitLab Training Material
+
+All GitLab training material is stored in markdown format. Slides are
+generated using [Deskset](http://www.decksetapp.com/).
+
+All training material is open to public contribution.
diff --git a/doc/university/training/logo.png b/doc/university/training/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc831790405f75591eac6b708a4fbc34671ce667
Binary files /dev/null and b/doc/university/training/logo.png differ
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
new file mode 100755
index 0000000000000000000000000000000000000000..1ee615432aaa566dc7dc83b5e1bda3ade0e0dbf7
--- /dev/null
+++ b/doc/university/training/topics/additional_resources.md
@@ -0,0 +1,8 @@
+## Additional Resources
+
+1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+5. Code School tutorial [http://try.github.io/](http://try.github.io/)
+6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
new file mode 100755
index 0000000000000000000000000000000000000000..e6e4fea9b51b29e7f08bd0571ba1c5032455a722
--- /dev/null
+++ b/doc/university/training/topics/agile_git.md
@@ -0,0 +1,33 @@
+# Agile and Git
+
+----------
+
+## Agile
+
+Lean software development methods focused on collaboration and interaction
+with fast and smaller deployment cycles.
+
+----------
+
+## Where Git comes in
+
+Git is an excellent tool for an Agile team considering that it allows
+decentralized and simultaneous development.
+
+----------
+
+### Branching And Workflows
+
+Branching in an Agile environment usually happens around user stories with one
+or more developers working on it.
+
+If more than one developer then another branch for each developer is also used
+with his/her initials, and US id.
+
+After its tested merge into master and remove the branch.
+
+----------
+
+## What about GitLab
+Tools like GitLab enhance collaboration by adding dialog around code mainly
+through issues and merge requests.
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
new file mode 100755
index 0000000000000000000000000000000000000000..a60c4365e0c75dd8dae4a20a351b72335cf5bbc7
--- /dev/null
+++ b/doc/university/training/topics/bisect.md
@@ -0,0 +1,81 @@
+# Bisect	  	
+
+----------
+
+## Bisect
+
+- Find a commit that introduced a bug
+- Works through a process of elimination
+- Specify a known good and bad revision to begin	  	
+
+----------
+
+## Bisect
+
+1. Start the bisect process
+2. Enter the bad revision (usually latest commit)
+3. Enter a known good revision (commit/branch)
+4. Run code to see if bug still exists
+5. Tell bisect the result
+6. Repeat the previous 2 items until you find the offending commit
+
+----------
+
+## Setup
+
+```
+  mkdir bisect-ex
+  cd bisect-ex
+  touch index.html
+  git add -A
+  git commit -m "starting out"
+  vi index.html
+  # Add all good
+  git add -A
+  git commit -m "second commit"
+  vi index.html
+  # Add all good 2
+  git add -A
+  git commit -m "third commit"
+  vi index.html
+```
+
+----------
+
+```
+  # Add all good 3
+  git add -A
+  git commit -m "fourth commit"
+  vi index.html
+  # This looks bad
+  git add -A
+  git commit -m "fifth commit"
+  vi index.html
+  # Really bad
+  git add -A
+  git commit -m "sixth commit"
+  vi index.html
+  # again just bad
+  git add -A
+  git commit -m "seventh commit"
+```
+
+----------
+
+## Commands
+
+```
+  git bisect start
+  # Test your code
+  git bisect bad
+  git bisect next
+  # Say yes to the warning
+  # Test
+  git bisect good
+  # Test
+  git bisect bad
+  # Test
+  git bisect good
+  # done
+  git bisect reset
+```
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
new file mode 100755
index 0000000000000000000000000000000000000000..af7a70a28187d7806f12fb09194e064066e17d8d
--- /dev/null
+++ b/doc/university/training/topics/cherry_picking.md
@@ -0,0 +1,39 @@
+# Cherry Pick
+
+----------
+
+## Cherry Pick
+
+- Given an existing commit on one branch, apply the change to another branch
+- Useful for backporting bug fixes to previous release branches
+- Make the commit on the master branch and pick in to stable
+
+----------
+
+## Cherry Pick
+
+1. Check out a new 'stable' branch from 'master'
+1. Change back to 'master'
+1. Edit '`cherry_pick.rb`' and commit the changes.
+1. Check commit log to get the commit SHA
+1. Check out the 'stable' branch
+1. Cherry pick the commit using the SHA obtained earlier
+
+----------
+
+## Commands
+
+```bash
+git checkout master
+git checkout -b stable
+git checkout master
+
+# Edit `cherry_pick.rb`
+git add cherry_pick.rb
+git commit -m 'Fix bugs in cherry_pick.rb'
+git log
+# Copy commit SHA
+git checkout stable
+
+git cherry-pick <commit SHA>
+```
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
new file mode 100755
index 0000000000000000000000000000000000000000..8149379b36f654e0414939a8e2546dca85b31015
--- /dev/null
+++ b/doc/university/training/topics/env_setup.md
@@ -0,0 +1,60 @@
+# Configure your environment
+
+----------
+## Install
+
+- **Windows**
+  - Install 'Git for Windows' from https://git-for-windows.github.io
+
+- **Mac**
+  - Type '`git`' in the Terminal application.
+  - If it's not installed, it will prompt you to install it.
+
+- **Linux**
+  ```bash
+    sudo yum install git-all
+  ```
+  ```bash
+    sudo apt-get install git-all
+  ```
+
+----------
+
+## Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+----------
+
+## Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
new file mode 100755
index 0000000000000000000000000000000000000000..b65457728c0c48cc0321a3be82ee94fcf076c46c
--- /dev/null
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -0,0 +1,10 @@
+# Explore GitLab projects
+
+----------
+
+- Dashboard
+- User Preferences
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
new file mode 100755
index 0000000000000000000000000000000000000000..4b34406ea753d6e4d55b2f9c4564cdf2b59f7738
--- /dev/null
+++ b/doc/university/training/topics/feature_branching.md
@@ -0,0 +1,32 @@
+# Feature branching
+
+----------
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+  - Hint: This is a cheap backup for your work-in-progress code
+
+----------
+
+## Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+----------
+
+## Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
new file mode 100755
index 0000000000000000000000000000000000000000..ec7bb2631aa4bc4bb3c2ba69d2cc7fb389b8184f
--- /dev/null
+++ b/doc/university/training/topics/getting_started.md
@@ -0,0 +1,95 @@
+# Getting Started
+
+----------
+
+## Instantiating Repositories
+
+* Create a new repository by instantiating it through
+```bash
+git init
+```
+* Copy an existing project by cloning the repository through
+```bash
+git clone <url>
+```
+
+----------
+
+## Central Repos
+
+* To instantiate a central repository a `--bare` flag is required.
+* Bare repositories don't allow file editing or committing changes.
+* Create a bare repo with
+```bash
+git init --bare project-name.git
+```
+
+----------
+
+## Instantiate workflow with clone
+
+1. Create a project in your user namespace
+  - Choose to import from 'Any Repo by URL' and use
+    https://gitlab.com/gitlab-org/training-examples.git
+2. Create a '`Workspace`' directory in your home directory.
+3. Clone the '`training-examples`' project
+
+----------
+
+## Commands
+
+```
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+----------
+
+## Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+----------
+
+## Committing Workflow
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+----------
+
+## Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+----------
+
+## Note
+
+* git fetch vs pull
+* Pull is git fetch + git merge
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
new file mode 100755
index 0000000000000000000000000000000000000000..9ffb4b9c8590f4d3fe48aeb6ade61ef49a0bd1dd
--- /dev/null
+++ b/doc/university/training/topics/git_add.md
@@ -0,0 +1,33 @@
+# Git Add
+
+----------
+
+## Git Add
+
+Adds content to the index or staging area.
+
+* Adds a list of file
+```bash
+git add <files>
+```
+* Adds all files including deleted ones
+```bash
+git add -A
+```
+
+----------
+
+## Git add continued
+
+* Add all text files in current dir
+```bash
+git add *.txt
+```
+* Add all text file in the project
+```bash
+git add "*.txt*"
+```
+* Adds all files in directory
+```bash
+git add views/layouts/
+```
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
new file mode 100755
index 0000000000000000000000000000000000000000..ca1ff29d93bc5fa42205196b39e67972fac9fafd
--- /dev/null
+++ b/doc/university/training/topics/git_intro.md
@@ -0,0 +1,24 @@
+# Git introduction
+
+----------
+
+## Intro
+
+https://git-scm.com/about
+
+- Distributed version control
+  - Does not rely on connection to a central server
+  - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+----------
+
+## Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
new file mode 100755
index 0000000000000000000000000000000000000000..32ebceff491d17cd8616f4678b973de60663ff40
--- /dev/null
+++ b/doc/university/training/topics/git_log.md
@@ -0,0 +1,57 @@
+# Git Log
+
+----------
+
+Git log lists commit history. It allows searching and filtering.
+
+* Initiate log
+```
+git log
+```
+
+* Retrieve set number of records:
+```
+git log -n 2
+```
+
+* Search commits by author. Allows user name or a regular expression.
+```
+git log --author="user_name"
+```
+
+----------
+
+* Search by comment message.
+```
+git log --grep="<pattern>"
+```
+
+* Search by date
+```
+git log --since=1.month.ago --until=3.weeks.ago
+```
+
+
+----------
+
+## Git Log Workflow
+
+1. Change to workspace directory
+2. Clone the multi runner projects
+3. Change to project dir
+4. Search by author
+5. Search by date
+6. Combine
+
+----------
+
+## Commands
+
+```
+cd ~/workspace
+git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
+cd gitlab-ci-multi-runner
+git log --author="Travis"
+git log --since=1.month.ago --until=3.weeks.ago
+git log --since=1.month.ago --until=1.day.ago --author="Travis"
+```
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
new file mode 100755
index 0000000000000000000000000000000000000000..8e5d3baf9595809c438c99d9555c03d56231e9e3
--- /dev/null
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+----------
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+----------
+
+### Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
+
+----------
+
+## Production branch
+
+- One, long-running production release branch
+  as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+----------
+
+## Production branch
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
+
+----------
+
+## Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+  from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+----------
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
+
+----------
+
+## More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
new file mode 100755
index 0000000000000000000000000000000000000000..77807b3e7eff4bd9a04327eacefdbf5cfc6894f3
--- /dev/null
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -0,0 +1,70 @@
+# Merge conflicts
+
+----------
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+----------
+
+## Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the `conflicts.rb` file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+----------
+
+## Commands
+
+```
+git checkout -b conflicts_branch
+
+# vi conflicts.rb
+# Add 'Line4' and 'Line5'
+
+git commit -am "add line4 and line5"
+git push origin conflicts_branch
+
+git checkout master
+
+# vi conflicts.rb
+# Add 'Line6' and 'Line7'
+git commit -am "add line6 and line7"
+git push origin master
+```
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+```
+git checkout conflicts_branch
+git fetch
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+# No need to commit this file
+
+git rebase --continue
+
+# Remember that we have rewritten our commit history so we
+# need to force push so that our remote branch is restructured
+git push origin conflicts_branch -f
+```
+----------
+
+## Note
+* When to use 'git merge' and when to use 'git rebase'
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
new file mode 100755
index 0000000000000000000000000000000000000000..5b446f02f6355376485dd7f17a5b1f0f40a96a8f
--- /dev/null
+++ b/doc/university/training/topics/merge_requests.md
@@ -0,0 +1,43 @@
+# Merge requests
+
+----------
+
+- When you want feedback create a merge request
+- Target is the default branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+----------
+
+## Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+----------
+
+## Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+----------
+
+## Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
new file mode 100755
index 0000000000000000000000000000000000000000..cf647284604ab6a84d119671a567e9b20d9f41e9
--- /dev/null
+++ b/doc/university/training/topics/rollback_commits.md
@@ -0,0 +1,81 @@
+# Rollback Commits
+
+----------
+
+## Undo Commits
+
+* Undo last commit putting everything back into the staging area.
+```
+git reset --soft HEAD^
+```
+
+* Add files and change message with:
+```
+git commit --amend -m "New Message"
+```
+
+----------
+
+* Undo last and remove changes
+```
+git reset --hard HEAD^
+```
+
+* Same as last one but for two commits back
+```
+git reset --hard HEAD^^
+```
+
+** Don't reset after pushing **
+
+----------
+
+## Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+
+----------
+
+## Commands
+
+```
+# Change file edit_this_file.rb
+git status
+git commit -am "kjkfjkg"
+git log
+git commit --amend -m "New comment added"
+git log
+git reset --soft HEAD^
+git log
+git pull origin master
+git push origin master
+```
+
+----------
+
+## Note
+
+* git revert vs git reset
+* Reset removes the commit while revert removes the changes but leaves the commit
+* Revert is safer considering we can revert a revert
+
+```
+# Changed file
+git commit -am "bug introduced"
+git revert HEAD
+# New commit created reverting changes
+# Now we want to re apply the reverted commit
+git log # take hash from the revert commit
+git revert <rev commit hash>
+# reverted commit is back (new commit created again)
+```
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
new file mode 100755
index 0000000000000000000000000000000000000000..c1bdda3264503f3993bbfc50d08d49efea805993
--- /dev/null
+++ b/doc/university/training/topics/stash.md
@@ -0,0 +1,86 @@
+# Git Stash
+
+----------
+
+We use git stash to store our changes when they are not ready to be committed
+and we need to change to a different branch.
+
+* Stash
+```
+git stash save
+# or
+git stash
+# or with a message
+git stash save "this is a message to display on the list"
+```
+
+* Apply stash to keep working on it
+```
+git stash apply
+# or apply a specific one from out stack
+git stash apply stash@{3}
+```
+
+----------
+
+* Every time we save a stash it gets stacked so by using list we can see all our
+stashes.
+
+```
+git stash list
+# or for more information (log methods)
+git stash list --stat
+```
+
+* To clean our stack we need to manually remove them.
+
+```
+# drop top stash
+git stash drop
+# or
+git stash drop <name>
+# to clear all history we can use
+git stash clear
+```
+
+----------
+
+* Apply and drop on one command
+
+```
+  git stash pop
+```
+
+* If we meet conflicts we need to either reset or commit our changes.
+
+* Conflicts through `pop` will not drop a stash afterwards.
+
+----------
+
+## Git Stash
+
+1. Modify a file
+2. Stage file
+3. Stash it
+4. View our stash list
+5. Confirm no pending changes through status
+5. Apply with pop
+6. View list to confirm changes
+
+----------
+
+## Commands
+
+```
+# Modify edit_this_file.rb file
+git add .
+
+git stash save "Saving changes from edit this file"
+
+git stash list
+git status
+
+git stash pop
+git stash list
+git status
+```
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
new file mode 100755
index 0000000000000000000000000000000000000000..5d869af64c15d9ab5a6498deb594403493f837d8
--- /dev/null
+++ b/doc/university/training/topics/subtree.md
@@ -0,0 +1,55 @@
+## Subtree
+
+----------
+
+## Subtree
+
+* Used when there are nested repositories.
+* Not recommended when the amount of dependencies is too large
+* For these cases we need a dependency control system
+* Command are painfully long so aliases are necessary
+
+----------
+
+## Subtree Aliases
+
+* Add: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Pull: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Push: git subtree add --prefix <target-folder> <url> <branch>
+* Ex: git config alias.sbp 'subtree pull --prefix st /
+  git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+
+----------
+
+```
+  # Add an alias
+  # Add
+  git config alias.sba 'subtree add --prefix st /
+  git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+  # Pull
+  git config alias.sbpl 'subtree pull --prefix st /
+  git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+  # Push
+  git config alias.sbph 'subtree push --prefix st /
+  git@gitlab.com:balameb/subtree-nested-example.git master'
+
+  # Adding this subtree adds a st dir with a readme
+  git sba
+  vi st/README.md
+  # Edit file
+  git status shows differences
+
+```
+
+----------
+
+```
+  # Adding, or committing won't change the sub repo at remote
+  # even if we push
+  git add -A
+  git commit -m "Adding to subtree readme"
+
+  # Push to subtree repo
+  git sbph
+  # now we can check our remote sub repo
+```
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
new file mode 100755
index 0000000000000000000000000000000000000000..e9607b5a875dbc28918111c0b901322864f3c367
--- /dev/null
+++ b/doc/university/training/topics/tags.md
@@ -0,0 +1,38 @@
+# Tags
+
+----------
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+----------
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+----------
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
new file mode 100755
index 0000000000000000000000000000000000000000..17dbb64b9e6bcba1c2ab237dbc0dda39d563298a
--- /dev/null
+++ b/doc/university/training/topics/unstage.md
@@ -0,0 +1,31 @@
+# Unstage
+
+----------
+
+## Unstage
+
+* To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
+
+```bash
+git reset HEAD <file>
+```
+
+* This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+```bash
+git checkout -- <file>
+```
+
+----------
+
+* To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag.
+```
+git rm '*.txt'
+git rm -r <dirname>
+```
+
+
+* If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`.
+```
+git rm <filename> --cache
+```
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
new file mode 100755
index 0000000000000000000000000000000000000000..35afe73708f1fbbdba0ea49282b1f328f8b1a483
--- /dev/null
+++ b/doc/university/training/user_training.md
@@ -0,0 +1,392 @@
+# GitLab Git Workshop
+
+---
+
+# Agenda
+
+1. Brief history of Git
+1. GitLab walkthrough
+1. Configure your environment
+1. Workshop
+
+---
+
+# Git introduction
+
+https://git-scm.com/about
+
+- Distributed version control
+  - Does not rely on connection to a central server
+  - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+---
+
+# Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
+
+---
+
+# GitLab Walkthrough
+
+![fit](logo.png)
+
+---
+
+# Configure your environment
+
+- Windows: Install 'Git for Windows'
+
+> https://git-for-windows.github.io
+
+- Mac: Type '`git`' in the Terminal application.
+
+> If it's not installed, it will prompt you to install it.
+
+- Debian: '`sudo apt-get install git-all`'
+or Red Hat '`sudo yum install git-all`'
+
+---
+
+# Git Workshop
+
+## Overview
+
+1. Configure Git
+1. Configure SSH Key
+1. Create a project
+1. Committing
+1. Feature branching
+1. Merge requests
+1. Feedback and Collaboration
+
+---
+
+# Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+---
+
+# Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
+
+---
+
+# Create a project
+
+- Create a project in your user namespace
+  - Choose to import from 'Any Repo by URL' and use
+    https://gitlab.com/gitlab-org/training-examples.git
+- Create a '`development`' or '`workspace`' directory in your home directory.
+- Clone the '`training-examples`' project
+
+---
+
+# Commands
+
+```
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+
+---
+
+# Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+---
+
+# Committing
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+---
+
+# Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---
+
+# Feature branching
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+  - Hint: This is a cheap backup for your work-in-progress code
+
+---
+
+# Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+---
+
+# Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+# Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+---
+
+# Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+---
+
+# Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+---
+
+# Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+# Explore GitLab projects
+
+![fit](logo.png)
+
+- Dashboard
+- User Preferences
+- ReadMe, Changelog, License shortcuts
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
+
+---
+
+# Tags
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+---
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+---
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
+
+---
+
+# Merge conflicts
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+---
+
+# Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+1. Commit and push
+1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+1. Commit and push to master
+1. Create a merge request
+
+---
+
+# Merge conflicts
+
+After creating a merge request you should notice that conflicts exist. Resolve
+the conflicts locally by rebasing.
+
+```
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+git commit -m 'Fix conflicts'
+git rebase --continue
+git push origin <branch> -f
+```
+
+---
+
+# Rebase with squash
+
+You may end up with a commit log that looks like this:
+
+```
+Fix issue #13
+Test
+Fix
+Fix again
+Test
+Test again
+Does this work?
+```
+
+Squash these in to meaningful commits using an interactive rebase.
+
+---
+
+# Rebase with squash
+
+Squash the commits on the same branch we used for the merge conflicts step.
+
+```
+git rebase -i master
+```
+
+In the editor, leave the first commit as 'pick' and set others to 'fixup'.
+
+---
+
+# Questions?
+
+![fit](logo.png)
+
+Thank you for your hard work!
+
+**Additional Resources**
+
+GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+Code School tutorial [http://try.github.io/](http://try.github.io/)
+Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index d57c0d0674d44cdfbc3ce68fffd57b803816009d..bfb83cf79b1f0815d3a439120e3c2e060dc19861 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -99,6 +99,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
 # Update init.d script
 sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
 ```
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 7. Update configuration files
 
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 46dfa2232b44e776c75927354b3a435b07654efd..7f36ce00e96b30e222d00cb71f35c9d86f1dba57 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -116,6 +116,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
 # Update init.d script
 sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
 ```
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 7. Update configuration files
 
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 84c624cbcb7006c94d7249e9007710cb37fc9d34..119c5f475e43416ef916b89899fe06bca0970b5e 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -20,7 +20,33 @@ cd /home/git/gitlab
 sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
 ```
 
-### 3. Get latest code
+### 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.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711  ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
 
 ```bash
 sudo -u git -H git fetch --all
@@ -41,15 +67,15 @@ For GitLab Enterprise Edition:
 sudo -u git -H git checkout 8-11-stable-ee
 ```
 
-### 4. Update gitlab-shell
+### 5. Update gitlab-shell
 
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.3.3
+sudo -u git -H git checkout v3.4.0
 ```
 
-### 5. Update gitlab-workhorse
+### 6. 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
@@ -58,11 +84,11 @@ GitLab 8.1.
 ```bash
 cd /home/git/gitlab-workhorse
 sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.7.8
+sudo -u git -H git checkout v0.7.11
 sudo -u git -H make
 ```
 
-### 6. Install libs, migrations, etc.
+### 7. Install libs, migrations, etc.
 
 ```bash
 cd /home/git/gitlab
@@ -84,7 +110,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
 
 ```
 
-### 7. Update configuration files
+### 8. Update configuration files
 
 #### New configuration options for `gitlab.yml`
 
@@ -132,13 +158,17 @@ See [smtp_settings.rb.sample] as an example.
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
-### 8. Start application
+### 9. Start application
 
     sudo service gitlab start
     sudo service nginx restart
 
-### 9. Check application status
+### 10. Check application status
 
 Check if GitLab and its environment are configured correctly:
 
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
new file mode 100644
index 0000000000000000000000000000000000000000..cddfa7e3e0111b11cde74e4afd954830b3cea066
--- /dev/null
+++ b/doc/update/8.11-to-8.12.md
@@ -0,0 +1,205 @@
+# From 8.11 to 8.12
+
+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
+
+    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.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711  ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+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
+sudo -u git -H git checkout 8-12-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-12-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.6.1
+```
+
+### 6. 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-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.8.2
+sudo -u git -H make
+```
+
+### 7. 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
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-11-stable:config/gitlab.yml.example origin/8-12-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+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
+# For HTTPS configurations
+git diff origin/8-11-stable:lib/support/nginx/gitlab-ssl origin/8-12-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-11-stable:lib/support/nginx/gitlab origin/8-12-stable:lib/support/nginx/gitlab
+```
+
+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/8-12-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-12-stable/config/initializers/smtp_settings.rb.sample#L13?
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+    sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
+
+### 9. Start application
+
+    sudo service gitlab start
+    sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+    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:
+
+    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.11)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.10 to 8.11](8.10-to-8.11.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.
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
new file mode 100644
index 0000000000000000000000000000000000000000..c0084d9d59c21add44506acc5c56fa000dd0e592
--- /dev/null
+++ b/doc/update/8.12-to-8.13.md
@@ -0,0 +1,205 @@
+# From 8.12 to 8.13
+
+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
+
+    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.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711  ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+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
+sudo -u git -H git checkout 8-13-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-13-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.6.6
+```
+
+### 6. 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-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.8.5
+sudo -u git -H make
+```
+
+### 7. 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
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-12-stable:config/gitlab.yml.example origin/8-13-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+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
+# For HTTPS configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab-ssl origin/8-13-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab origin/8-13-stable:lib/support/nginx/gitlab
+```
+
+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/8-13-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-13-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+    sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
+
+### 9. Start application
+
+    sudo service gitlab start
+    sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+    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:
+
+    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.12)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.11 to 8.12](8.11-to-8.12.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.
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
new file mode 100644
index 0000000000000000000000000000000000000000..46ea19d11d0c422c92163c9647b3dc06a32ec9ef
--- /dev/null
+++ b/doc/update/8.13-to-8.14.md
@@ -0,0 +1,205 @@
+# From 8.13 to 8.14
+
+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
+
+    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.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711  ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+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
+sudo -u git -H git checkout 8-14-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-14-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.0.0
+```
+
+### 6. 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-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v1.0.0
+sudo -u git -H make
+```
+
+### 7. 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
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-13-stable:config/gitlab.yml.example origin/8-14-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+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
+# For HTTPS configurations
+git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-14-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-14-stable:lib/support/nginx/gitlab
+```
+
+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/8-14-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-14-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+    sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
+
+### 9. Start application
+
+    sudo service gitlab start
+    sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+    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:
+
+    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.13)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.12 to 8.13](8.12-to-8.13.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.
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 9f5c6c4dc84ba652c11bab96406f32601c9a277f..dd3fdafd8d19d094bb7264248b4cd3f97dd5d1e9 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -158,6 +158,10 @@ it where the 'public' directory of GitLab is.
 cd /home/git/gitlab
 sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
 ```
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 8. Use Redis v2.8.0+
 
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 9f6517d9487e636e0b10027bae5dcfaf2545519f..e62d894609a563fa873eedaebb8c845dcfc717ed 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -98,6 +98,10 @@ We updated the init script for GitLab in order to set a specific PATH for gitlab
 cd /home/git/gitlab
 sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
 ```
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 8. Start application
 
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0cb137a03cc337cd609873864d9f375c760b6c28..678cc69d7738c9de6206aab9ecb52de2fd434239 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -119,6 +119,10 @@ via [/etc/default/gitlab].
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 8. Start application
 
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 6267f14eba4626c53f157da545ac0d519f217a42..a76346516b9bc0a75bf59108e2b94e9be7992366 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -138,6 +138,10 @@ via [/etc/default/gitlab].
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 9. Start application
 
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index cb66ef920bb95ee67b928106e8606233bf12b4a5..05ef4e617593fcc1b6fb51aa134cd9b62dcb9ed3 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 8. Start application
 
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 32906650f6f1fddc2d7eff06de06c0856db4c79c..8ce434e5f78f04e3e1ab1625ab9594d2e03e1922 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 8. Start application
 
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index f078a2bece52141ecefedd0f32b706f222a09fbe..aa077316bbe9c9041e474ba026f4465308204024 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 9. Start application
 
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index a057a423e61b6d83718552f986873f35850585b8..bb2c79fbb844bc37a7d9e044a0b35e6227f49c5d 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
 Ensure you're still up-to-date with the latest init script changes:
 
     sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+    
+For Ubuntu 16.04.1 LTS:
+
+    sudo systemctl daemon-reload
 
 ### 9. Start application
 
diff --git a/doc/update/README.md b/doc/update/README.md
index 975d72164b4eb050ff53b5e56cd87ca7ec9af8f1..837b31abb979e4161643d7965f5f49e6fc814be8 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -85,6 +85,8 @@ possible.
 - [MySQL installation guide](../install/database_mysql.md) contains additional
   information about configuring GitLab to work with a MySQL database.
 - [Restoring from backup after a failed upgrade](restore_after_failure.md)
+- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for
+  upgrading a PostgreSQL database with minimal downtime.
 
 [omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html
 [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update
diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md
new file mode 100644
index 0000000000000000000000000000000000000000..f009906256e4ed5a68239bf01bfbe6a6e414b438
--- /dev/null
+++ b/doc/update/upgrading_postgresql_using_slony.md
@@ -0,0 +1,482 @@
+# Upgrading PostgreSQL Using Slony
+
+This guide describes the steps one can take to upgrade their PostgreSQL database
+to the latest version without the need for hours of downtime. This guide assumes
+you have two database servers: one database server running an older version of
+PostgreSQL (e.g. 9.2.18) and one server running a newer version (e.g. 9.6.0).
+
+For this process we'll use a PostgreSQL replication tool called
+["Slony"](http://www.slony.info/). Slony allows replication between different
+PostgreSQL versions and as such can be used to upgrade a cluster with a minimal
+amount of downtime.
+
+In various places we'll refer to the user `gitlab-psql`. This user should be the
+user used to run the various PostgreSQL OS processes. If you're using a
+different user (e.g. `postgres`) you should replace `gitlab-psql` with the name
+of said user. This guide also assumes your database is called
+`gitlabhq_production`. If you happen to use a different database name you should
+change this accordingly.
+
+## Database Dumps
+
+Slony only replicates data and not any schema changes. As a result we must
+ensure that all databases have the same database structure.
+
+To do so we'll generate a dump of our current database. This dump will only
+contain the structure, not any data. To generate this dump run the following
+command on your active database server:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql -p 5432 -U gitlab-psql -s -f /tmp/structure.sql gitlabhq_production
+```
+
+If you're not using GitLab's Omnibus package you may have to adjust the paths to
+`pg_dump` and the PostgreSQL installation directory to match the paths of your
+configuration.
+
+Once the structure dump is generated we also need to generate a dump for the
+`schema_migrations` table. This table doesn't have any primary keys and as such
+can't be replicated easily by Slony. To generate this dump run the following
+command on your active database server:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -a -t schema_migrations -f /tmp/migrations.sql gitlabhq_production
+```
+
+Next we'll need to move these files somewhere accessible by the new database
+server. The easiest way is to simply download these files to your local system:
+
+```bash
+scp your-user@production-database-host:/tmp/*.sql /tmp
+```
+
+This will copy all the SQL files located in `/tmp` to your local system's
+`/tmp` directory. Once copied you can safely remove the files from the database
+server.
+
+## Installing Slony
+
+Slony will be used to upgrade the database without requiring long downtimes.
+Slony can be downloaded from http://www.slony.info/. If you have installed
+PostgreSQL using your operating system's package manager you may also be able to
+install Slony using said package manager.
+
+When compiling Slony from source you *must* use the following commands to do so:
+
+```bash
+./configure --prefix=/path/to/installation/directory --with-perltools --with-pgconfigdir=/path/to/directory/containing/pg_config/bin
+make
+make install
+```
+
+Omnibus users can use the following commands:
+
+```bash
+./configure --prefix=/opt/gitlab/embedded --with-perltools --with-pgconfigdir=/opt/gitlab/embedded/bin
+make
+make install
+```
+
+This assumes you have installed GitLab into /opt/gitlab.
+
+To test if Slony is installed properly, run the following commands:
+
+```bash
+test -f /opt/gitlab/embedded/bin/slonik && echo 'Slony installed' || echo 'Slony not installed'
+test -f /opt/gitlab/embedded/bin/slonik_init_cluster && echo 'Slony Perl tools are available' || echo 'Slony Perl tools are not available'
+/opt/gitlab/embedded/bin/slonik -v
+```
+
+This assumes Slony was installed to `/opt/gitlab/embedded`. If Slony was
+installed properly the output of these commands will be (the mentioned "slonik"
+version may be different):
+
+```
+Slony installed
+Slony Perl tools are available
+slonik version 2.2.5
+```
+
+## Slony User
+
+Next we must set up a PostgreSQL user that Slony can use to replicate your
+database. To do so, log in to your production database using `psql` using a
+super user account. Once done run the following SQL queries:
+
+```sql
+CREATE ROLE slony WITH SUPERUSER LOGIN REPLICATION ENCRYPTED PASSWORD 'password string here';
+ALTER ROLE slony SET statement_timeout TO 0;
+```
+
+Make sure you replace "password string here" with the actual password for the
+user. A password is *required*. This user must be created on _both_ the old and
+new database server using the same password.
+
+Once the user has been created make sure you note down the password as we will
+need it later on.
+
+## Configuring Slony
+
+Now we can finally start configuring Slony. Slony uses a configuration file for
+most of the work so we'll need to set this one up. This configuration file
+specifies where to put log files, how Slony should connect to the databases,
+etc.
+
+First we'll need to create some required directories and set the correct
+permissions. To do so, run the following commands on both the old and new
+database server:
+
+```bash
+sudo mkdir -p /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
+sudo chown gitlab-psql:root /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
+```
+
+Here `gitlab-psql` is the user used to run the PostgreSQL database processes. If
+you're using a different user you should replace this with the name of said
+user.
+
+Now that the directories are in place we can create the configuration file. For
+this we can use the following template:
+
+```perl
+if ($ENV{"SLONYNODES"}) {
+    require $ENV{"SLONYNODES"};
+} else {
+    $CLUSTER_NAME = 'slony_replication';
+    $LOGDIR = '/var/log/gitlab/slony';
+    $MASTERNODE = 1;
+    $DEBUGLEVEL = 2;
+
+    add_node(host => 'OLD_HOST', dbname => 'gitlabhq_production', port =>5432,
+        user=>'slony', password=>'SLONY_PASSWORD', node=>1);
+
+    add_node(host => 'NEW_HOST', dbname => 'gitlabhq_production', port =>5432,
+        user=>'slony', password=>'SLONY_PASSWORD', node=>2, parent=>1 );
+}
+
+$SLONY_SETS = {
+    "set1" => {
+        "set_id"       => 1,
+        "table_id"     => 1,
+        "sequence_id"  => 1,
+        "pkeyedtables" => [
+            TABLES
+        ],
+    },
+};
+
+if ($ENV{"SLONYSET"}) {
+    require $ENV{"SLONYSET"};
+}
+
+# Please do not add or change anything below this point.
+1;
+```
+
+In this configuration file you should replace a few placeholders before you can
+use it. The following placeholders should be replaced:
+
+* `OLD_HOST`: the address of the old database server.
+* `NEW_HOST`: the address of the new database server.
+* `SLONY_PASSWORD`: the password of the Slony user created earlier.
+* `TABLES`: the tables to replicate.
+
+The list of tables to replicate can be generated by running the following
+command on your old PostgreSQL database:
+
+```
+sudo gitlab-psql gitlabhq_production -c "select concat('\"', schemaname, '.', tablename, '\",') from pg_catalog.pg_tables where schemaname = 'public' and tableowner = 'gitlab' and tablename != 'schema_migrations' order by tablename asc;" -t
+```
+
+If you're not using Omnibus you should replace `gitlab-psql` with the
+appropriate path to the `psql` executable.
+
+The above command outputs a list of tables in a format that can be copy-pasted
+directly into the above configuration file. Make sure to _replace_ `TABLES` with
+this output, don't just append it below it. Once done you'll end up with
+something like this:
+
+```perl
+"pkeyedtables" => [
+    "public.abuse_reports",
+    "public.appearances",
+    "public.application_settings",
+    ... more rows here ...
+]
+```
+
+Once you have the configuration file generated you must install it on both the
+old and new database. To do so, place it in
+`/var/opt/gitlab/postgresql/slony/slon_tools.conf` (for which we created the
+directory earlier on).
+
+Now that the configuration file is in place we can _finally_ start replicating
+our database. First we must set up the schema in our new database. To do so make
+sure that the SQL files we generated earlier can be found in the `/tmp`
+directory of the new server. Once these files are in place start a `psql`
+session on this server:
+
+```
+sudo gitlab-psql gitlabhq_production
+```
+
+Now run the following commands:
+
+```
+\i /tmp/structure.sql
+\i /tmp/migrations.sql
+```
+
+To verify if the structure is in place close the session, start it again, then
+run `\d`. If all went well you should see output along the lines of the
+following:
+
+```
+                               List of relations
+ Schema |                    Name                     |   Type   |    Owner
+--------+---------------------------------------------+----------+-------------
+ public | abuse_reports                               | table    | gitlab
+ public | abuse_reports_id_seq                        | sequence | gitlab
+ public | appearances                                 | table    | gitlab
+ public | appearances_id_seq                          | sequence | gitlab
+ public | application_settings                        | table    | gitlab
+ public | application_settings_id_seq                 | sequence | gitlab
+ public | approvals                                   | table    | gitlab
+ ... more rows here ...
+```
+
+Now we can initialize the required tables and what not that Slony will use for
+its replication process. To do so, run the following on the old database:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_init_cluster --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+If all went well this will produce something along the lines of:
+
+```
+<stdin>:10: Set up replication nodes
+<stdin>:13: Next: configure paths for each node/origin
+<stdin>:16: Replication nodes prepared
+<stdin>:17: Please start a slon replication daemon for each node
+```
+
+Next we need to start a replication node on every server. To do so, run the
+following on the old database:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+If all went well this will produce output such as:
+
+
+```
+Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2  slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 &
+Slon successfully started for cluster slony_replication, node node1
+PID [26740]
+Start the watchdog process as well...
+```
+
+Next we need to run the following command on the _new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+This will produce similar output if all went well.
+
+Next we need to tell the new database server what it should replicate. This can
+be done by running the following command on the _new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_create_set 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+This should produce output along the lines of the following:
+
+```
+<stdin>:11: Subscription set 1 (set1) created
+<stdin>:12: Adding tables to the subscription set
+<stdin>:16: Add primary keyed table public.abuse_reports
+<stdin>:20: Add primary keyed table public.appearances
+<stdin>:24: Add primary keyed table public.application_settings
+... more rows here ...
+<stdin>:327: Adding sequences to the subscription set
+<stdin>:328: All tables added
+```
+
+Finally we can start the replication process by running the following on the
+_new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_subscribe_set 1 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+This should produce the following output:
+
+```
+<stdin>:6: Subscribed nodes to set 1
+```
+
+At this point the new database server will start replicating the data of the old
+database server. This process can take anywhere from a few minutes to hours, if
+not days. Unfortunately Slony itself doesn't really provide a way of knowing
+when the two databases are in sync. To get an estimate of the progress you can
+use the following shell script:
+
+```
+#!/usr/bin/env bash
+
+set -e
+
+user='slony'
+pass='SLONY_PASSWORD'
+
+function main {
+    while :
+    do
+        local source
+        local target
+
+        source=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h OLD_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
+        target=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h NEW_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
+
+        echo "$(date): ${target} of ${source}" >> progress.log
+        echo "$(date): ${target} of ${source}"
+
+        sleep 60
+    done
+}
+
+main
+```
+
+This script will compare the sizes of the old and new database every minute and
+print the result to STDOUT as well as logging it to a file. Make sure to replace
+`SLONY_PASSWORD`, `OLD_HOST`, and `NEW_HOST` with the correct values.
+
+## Stopping Replication
+
+At some point the two databases are in sync. Once this is the case you'll need
+to plan for a few minutes of downtime. This small downtime window is used to
+stop the replication process, remove any Slony data from both databases, restart
+GitLab so it can use the new database, etc.
+
+First, let's stop all of GitLab. Omnibus users can do so by running the
+following on their GitLab server(s):
+
+```
+sudo gitlab-ctl stop unicorn
+sudo gitlab-ctl stop sidekiq
+sudo gitlab-ctl stop mailroom
+```
+
+If you have any other processes that use PostgreSQL you should also stop those.
+
+Once everything has been stopped you should update any configuration settings,
+DNS records, etc so they all point to the new database.
+
+Once the settings have been taken care of we need to stop the replication
+process. It's crucial that no new data is written to the databases at this point
+as this data will be lost.
+
+To stop replication, run the following on both database servers:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_kill --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+This will stop all the Slony processes on the host the command was executed on.
+
+## Resetting Sequences
+
+The above setup does not replicate database sequences, as such these must be
+reset manually in the target database. You can use the following script for
+this:
+
+```bash
+#!/usr/bin/env bash
+set -e
+
+function main {
+    local fix_sequences
+    local fix_owners
+
+    fix_sequences='/tmp/fix_sequences.sql'
+    fix_owners='/tmp/fix_owners.sql'
+
+    # The SQL queries were taken from
+    # https://wiki.postgresql.org/wiki/Fixing_Sequences
+    sudo gitlab-psql gitlabhq_production -t -c "
+    SELECT 'ALTER SEQUENCE '|| quote_ident(MIN(schema_name)) ||'.'|| quote_ident(MIN(seq_name))
+           ||' OWNED BY '|| quote_ident(MIN(TABLE_NAME)) ||'.'|| quote_ident(MIN(column_name)) ||';'
+    FROM (
+        SELECT
+            n.nspname AS schema_name,
+            c.relname AS TABLE_NAME,
+            a.attname AS column_name,
+            SUBSTRING(d.adsrc FROM E'^nextval\\(''([^'']*)''(?:::text|::regclass)?\\)') AS seq_name
+        FROM pg_class c
+        JOIN pg_attribute a ON (c.oid=a.attrelid)
+        JOIN pg_attrdef d ON (a.attrelid=d.adrelid AND a.attnum=d.adnum)
+        JOIN pg_namespace n ON (c.relnamespace=n.oid)
+        WHERE has_schema_privilege(n.oid,'USAGE')
+          AND n.nspname NOT LIKE 'pg!_%' escape '!'
+          AND has_table_privilege(c.oid,'SELECT')
+          AND (NOT a.attisdropped)
+          AND d.adsrc ~ '^nextval'
+    ) seq
+    GROUP BY seq_name HAVING COUNT(*)=1;
+    " > "${fix_owners}"
+
+    sudo gitlab-psql gitlabhq_production -t -c "
+    SELECT 'SELECT SETVAL(' ||
+           quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
+           ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
+           quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
+    FROM pg_class AS S,
+         pg_depend AS D,
+         pg_class AS T,
+         pg_attribute AS C,
+         pg_tables AS PGT
+    WHERE S.relkind = 'S'
+        AND S.oid = D.objid
+        AND D.refobjid = T.oid
+        AND D.refobjid = C.attrelid
+        AND D.refobjsubid = C.attnum
+        AND T.relname = PGT.tablename
+    ORDER BY S.relname;
+    " > "${fix_sequences}"
+
+    sudo gitlab-psql gitlabhq_production -f "${fix_owners}"
+    sudo gitlab-psql gitlabhq_production -f "${fix_sequences}"
+
+    rm "${fix_owners}" "${fix_sequences}"
+}
+
+main
+```
+
+Upload this script to the _target_ server and execute it as follows:
+
+```bash
+bash path/to/the/script/above.sh
+```
+
+This will correct the ownership of sequences and reset the next value for the
+`id` column to the next available value.
+
+## Removing Slony
+
+Next we need to remove all Slony related data. To do so, run the following
+command on the _target_ server:
+
+```bash
+sudo gitlab-psql gitlabhq_production -c "DROP SCHEMA _slony_replication CASCADE;"
+```
+
+Once done you can safely remove any Slony related files (e.g. the log
+directory), and uninstall Slony if desired. At this point you can start your
+GitLab instance again and if all went well it should be using your new database
+server.
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
new file mode 100644
index 0000000000000000000000000000000000000000..816094bf8d2e33135b680511c21cc786b8eac29a
--- /dev/null
+++ b/doc/user/account/security.md
@@ -0,0 +1,3 @@
+# Account Security
+
+- [Two-Factor Authentication](two_factor_authentication.md)
diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md
new file mode 100644
index 0000000000000000000000000000000000000000..881358ed94dd9156913b57f9597172b09e3dd89e
--- /dev/null
+++ b/doc/user/account/two_factor_authentication.md
@@ -0,0 +1,68 @@
+# Two-Factor Authentication
+
+## Recovery options
+
+If you lose your code generation device (such as your mobile phone) and you need
+to disable two-factor authentication on your account, you have several options.
+
+### Use a saved recovery code
+
+When you enabled two-factor authentication for your account, a series of
+recovery codes were generated. If you saved those codes somewhere safe, you
+may use one to sign in.
+
+First, enter your username/email and password on the GitLab sign in page. When
+prompted for a two-factor code, enter one of the recovery codes you saved
+previously.
+
+> **Note:** Once a particular recovery code has been used, it cannot be used again.
+  You may still use the other saved recovery codes at a later time.
+
+### Generate new recovery codes using SSH
+
+It's not uncommon for users to forget to save the recovery codes when enabling
+two-factor authentication. If you have an SSH key added to your GitLab account,
+you can generate a new set of recovery codes using SSH.
+
+Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
+confirm that you wish to generate new codes. If you choose to continue, any
+previously saved codes will be invalidated.
+
+```bash
+$ ssh git@gitlab.example.com 2fa_recovery_codes
+Are you sure you want to generate new two-factor recovery codes?
+Any existing recovery codes you saved will be invalidated. (yes/no)
+yes
+
+Your two-factor authentication recovery codes are:
+
+119135e5a3ebce8e
+11f6v2a498810dcd
+3924c7ab2089c902
+e79a3398bfe4f224
+34bd7b74adbc8861
+f061691d5107df1a
+169bf32a18e63e7f
+b510e7422e81c947
+20dbed24c5e74663
+df9d3b9403b9c9f0
+
+During sign in, use one of the codes above when prompted for
+your two-factor code. Then, visit your Profile Settings and add
+a new device so you do not lose access to your account again.
+```
+
+Next, go to the GitLab sign in page and enter your username/email and password.
+When prompted for a two-factor code, enter one of the recovery codes obtained
+from the command line output.
+
+> **Note:** After signing in, you should immediately visit your **Profile Settings
+  -> Account** to set up two-factor authentication with a new device.
+
+### Ask a GitLab administrator to disable two-factor on your account
+
+If the above two methods are not possible, you may ask a GitLab global
+administrator to disable two-factor authentication for your account. Please
+be aware that this will temporarily leave your account in a less secure state.
+You should sign in and re-enable two-factor authentication as soon as possible
+after the administrator disables it.
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
new file mode 100644
index 0000000000000000000000000000000000000000..eac57bc3de4b9da3e95b57c846f2a0bb85694139
--- /dev/null
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -0,0 +1,66 @@
+# Health Check
+
+> [Introduced][ce-3888] in GitLab 8.8.
+
+GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
+endpoint. The health check reports on the overall system status based on the status of
+the database connection, the state of the database migrations, and the ability to write
+and access the cache. This endpoint can be provided to uptime monitoring services like
+[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+
+## Access Token
+
+An access token needs to be provided while accessing the health check endpoint. The current
+accepted token can be found on the `admin/health_check` page of your GitLab instance.
+
+![access token](img/health_check_token.png)
+
+The access token can be passed as a URL parameter:
+
+```
+https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
+```
+
+or as an HTTP header:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+## Using the Endpoint
+
+Once you have the access token, health information can be retrieved as plain text, JSON,
+or XML using the `health_check` endpoint:
+
+- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
+
+You can also ask for the status of specific services:
+
+- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
+
+For example, the JSON output of the following health check:
+
+```bash
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+would be like:
+
+```
+{"healthy":true,"message":"success"}
+```
+
+## Status
+
+On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
+will return a valid successful HTTP status code, and a `success` message. Ideally your
+uptime monitoring should look for the success message.
+
+[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
+[pingdom]: https://www.pingdom.com
+[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
+[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
diff --git a/doc/monitoring/img/health_check_token.png b/doc/user/admin_area/monitoring/img/health_check_token.png
similarity index 100%
rename from doc/monitoring/img/health_check_token.png
rename to doc/user/admin_area/monitoring/img/health_check_token.png
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7fe96e67dbbfd9028b8f6f0693acace82bbd68ed..7a7a0b864bd1216e69783b632017c67505f58f26 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -27,6 +27,7 @@
 * [Horizontal Rule](#horizontal-rule)
 * [Line Breaks](#line-breaks)
 * [Tables](#tables)
+* [Footnotes](#footnotes)
 
 **[Wiki-Specific Markdown](#wiki-specific-markdown)**
 
@@ -66,7 +67,7 @@ dependency to do so. Please see the [github-markup gem readme](https://github.co
 ## Newlines
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
 
 GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
 
@@ -86,7 +87,7 @@ Sugar is sweet
 ## Multiple underscores in words
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiple-underscores-in-words
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
 
 It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
 
@@ -101,7 +102,7 @@ do_this_and_do_that_and_another_thing
 ## URL auto-linking
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#url-auto-linking
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking
 
 GFM will autolink almost any URL you copy and paste into your text:
 
@@ -122,7 +123,7 @@ GFM will autolink almost any URL you copy and paste into your text:
 ## Multiline Blockquote
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiline-blockquote
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
 
 On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
 GFM supports multiline blockquotes fenced by <code>>>></code>:
@@ -156,7 +157,7 @@ you can quote that without having to manually prepend `>` to every line!
 ## Code and Syntax Highlighting
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#code-and-syntax-highlighting
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting
 
 _GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
 list of supported languages visit the Rouge website._
@@ -226,7 +227,7 @@ But let's throw in a <b>tag</b>.
 ## Inline Diff
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#inline-diff
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
 
 With inline diffs tags you can display {+ additions +} or [- deletions -].
 
@@ -242,7 +243,7 @@ However the wrapping tags cannot be mixed as such:
 ## Emoji
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#emoji
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
 
 	Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
 
@@ -307,7 +308,7 @@ GFM also recognizes certain cross-project references:
 ## Task Lists
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#task-lists
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists
 
 You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
 
@@ -330,7 +331,7 @@ Task lists can only be created in descriptions, not in titles. Task item state c
 ## Videos
 
 > If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
 
 Image tags with a video extension are automatically converted to a video player.
 
@@ -500,6 +501,10 @@ There are two ways to create links, inline-style and reference-style.
     [I'm a reference-style link][Arbitrary case-insensitive reference text]
 
     [I'm a relative reference to a repository file](LICENSE)
+    
+    [I am an absolute reference within the repository](/doc/user/markdown.md)
+    
+    [I link to the Milestones page](/../milestones)
 
     [You can use numbers for reference-style link definitions][1]
 
@@ -517,6 +522,10 @@ There are two ways to create links, inline-style and reference-style.
 
 [I'm a relative reference to a repository file](LICENSE)[^1]
 
+[I am an absolute reference within the repository](/doc/user/markdown.md)
+    
+[I link to the Milestones page](/../milestones)
+    
 [You can use numbers for reference-style link definitions][1]
 
 Or leave it empty and use the [link text itself][]
@@ -699,6 +708,15 @@ By including colons in the header row, you can align the text within that column
 | Cell 1       | Cell 2   | Cell 3        | Cell 4       | Cell 5   | Cell 6        |
 | Cell 7       | Cell 8   | Cell 9        | Cell 10      | Cell 11  | Cell 12       |
 
+## Footnotes
+
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+
+```
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+```
 
 ## Wiki-specific Markdown
 
@@ -780,7 +798,7 @@ A link starting with a `/` is relative to the wiki root.
 - The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
 - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
 
-[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md
+[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
 [rouge]: http://rouge.jneen.net/ "Rouge website"
 [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
 [^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 665428617816f388698307e91e187df9910e1d50..d6216a8dd5047a6d17518005b1fd7c547c8c4786 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project.
 | See a list of builds                  | ✓ [^1]  | ✓          | ✓           | ✓        | ✓      |
 | See a build log                       | ✓ [^1]  | ✓          | ✓           | ✓        | ✓      |
 | Download and browse build artifacts   | ✓ [^1]  | ✓          | ✓           | ✓        | ✓      |
+| View wiki pages                       | ✓       | ✓          | ✓           | ✓        | ✓      |
 | Pull project code                     |         | ✓          | ✓           | ✓        | ✓      |
 | Download project                      |         | ✓          | ✓           | ✓        | ✓      |
 | Create code snippets                  |         | ✓          | ✓           | ✓        | ✓      |
@@ -31,6 +32,7 @@ The following table depicts the various user permission levels in a project.
 | See a commit status                   |         | ✓          | ✓           | ✓        | ✓      |
 | See a container registry              |         | ✓          | ✓           | ✓        | ✓      |
 | See environments                      |         | ✓          | ✓           | ✓        | ✓      |
+| See a list of merge requests          |         | ✓          | ✓           | ✓        | ✓      |
 | Manage/Accept merge requests          |         |            | ✓           | ✓        | ✓      |
 | Create new merge request              |         |            | ✓           | ✓        | ✓      |
 | Create new branches                   |         |            | ✓           | ✓        | ✓      |
@@ -63,7 +65,7 @@ The following table depicts the various user permission levels in a project.
 | Force push to protected branches [^2] |         |            |             |          |        |
 | Remove protected branches [^2]        |         |            |             |          |        |
 
-[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
 [^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
 
 ## Group
@@ -104,6 +106,15 @@ will find the option to flag the user as external.
 By default new users are not set as external users. This behavior can be changed
 by an administrator under **Admin > Application Settings**.
 
+## Project features
+
+Project features like wiki and issues can be hidden from users depending on
+which visibility level you select on project settings.
+
+- Disabled: disabled for everyone
+- Only team members: only team members will see even if your project is public or internal
+- Everyone with access: everyone can see depending on your project visibility level
+
 ## GitLab CI
 
 GitLab CI permissions rely on the role the user has in GitLab. There are four
@@ -129,3 +140,33 @@ instance and project. In addition, all admins can use the admin interface under
 | Add shared runners                    |                 |             |          | ✓      |
 | See events in the system              |                 |             |          | ✓      |
 | Admin interface                       |                 |             |          | ✓      |
+
+### Build permissions
+
+> Changed in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][new-mod].
+
+This table shows granted privileges for builds triggered by specific types of
+users:
+
+| Action                                      | Guest, Reporter | Developer   | Master   | Admin  |
+|---------------------------------------------|-----------------|-------------|----------|--------|
+| Run CI build                                |                 | ✓           | ✓        | ✓      |
+| Clone source and LFS from current project   |                 | ✓           | ✓        | ✓      |
+| Clone source and LFS from public projects   |                 | ✓           | ✓        | ✓      |
+| Clone source and LFS from internal projects |                 | ✓ [^3]      | ✓ [^3]   | ✓      |
+| Clone source and LFS from private projects  |                 | ✓ [^4]      | ✓ [^4]   | ✓ [^4] |
+| Push source and LFS                         |                 |             |          |        |
+| Pull container images from current project  |                 | ✓           | ✓        | ✓      |
+| Pull container images from public projects  |                 | ✓           | ✓        | ✓      |
+| Pull container images from internal projects|                 | ✓ [^3]      | ✓ [^3]   | ✓      |
+| Pull container images from private projects |                 | ✓ [^4]      | ✓ [^4]   | ✓ [^4] |
+| Push container images to current project    |                 | ✓           | ✓        | ✓      |
+| Push container images to other projects     |                 |             |          |        |
+
+[^3]: Only if user is not external one.
+[^4]: Only if user is a member of the project.
+[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
+[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
index c93ae1c369ca8892ff9ddf2c4ea910f0eb58baba..88f1863dddb7b2a13032fe30bb0cd923bd477b97 100644
--- a/doc/user/project/builds/artifacts.md
+++ b/doc/user/project/builds/artifacts.md
@@ -101,4 +101,36 @@ inside GitLab that make that possible.
 
     ![Build artifacts browser](img/build_artifacts_browser.png)
 
+## Downloading the latest build artifacts
+
+It is possible to download the latest artifacts of a build via a well known URL
+so you can use it for scripting purposes.
+
+The structure of the URL is the following:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
+```
+
+For example, to download the latest artifacts of the job named `rspec 6 20` of
+the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
+namespace, the URL would be:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
+```
+
+The latest builds are also exposed in the UI in various places. Specifically,
+look for the download button in:
+
+- the main project's page
+- the branches page
+- the tags page
+
+If the latest build has failed to upload the artifacts, you can see that
+information in the UI.
+
+![Latest artifacts button](img/build_latest_artifacts_browser.png)
+
+
 [gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/builds/img/build_latest_artifacts_browser.png
new file mode 100644
index 0000000000000000000000000000000000000000..d8e9071958c388be09f725813c6012e36779ddd3
Binary files /dev/null and b/doc/user/project/builds/img/build_latest_artifacts_browser.png differ
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
new file mode 100644
index 0000000000000000000000000000000000000000..b205fea2c40ca562b20870a64d19676b9e1a18df
--- /dev/null
+++ b/doc/user/project/container_registry.md
@@ -0,0 +1,253 @@
+# GitLab Container Registry
+
+> [Introduced][ce-4040] in GitLab 8.8.
+
+---
+
+> **Note**
+Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
+versions earlier than 1.10.
+>
+This document is about the user guide. To learn how to enable GitLab Container
+Registry across your GitLab instance, visit the
+[administrator documentation](../../administration/container_registry.md).
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+## Enable the Container Registry for your project
+
+1. First, ask your system administrator to enable GitLab Container Registry
+   following the [administration documentation](../../administration/container_registry.md).
+   If you are using GitLab.com, this is enabled by default so you can start using
+   the Registry immediately.
+
+1. Go to your project's settings and enable the **Container Registry** feature
+   on your project. For new projects this might be enabled by default. For
+   existing projects (prior GitLab 8.8), you will have to explicitly enable it.
+
+    ![Enable Container Registry](img/container_registry_enable.png)
+
+1. Hit **Save changes** for the changes to take effect. You should now be able
+   to see the **Registry** link in the project menu.
+
+    ![Container Registry tab](img/container_registry_tab.png)
+
+## Build and push images
+
+If you visit the **Registry** link under your project's menu, you can see the
+explicit instructions to login to the Container Registry using your GitLab
+credentials.
+
+For example if the Registry's URL is `registry.example.com`, the you should be
+able to login with:
+
+```
+docker login registry.example.com
+```
+
+Building and publishing images should be a straightforward process. Just make
+sure that you are using the Registry URL with the namespace and project name
+that is hosted on GitLab:
+
+```
+docker build -t registry.example.com/group/project .
+docker push registry.example.com/group/project
+```
+
+Your image will be named after the following scheme:
+
+```
+<registry URL>/<namespace>/<project>
+```
+
+As such, the name of the image is unique, but you can differentiate the images
+using tags.
+
+## Use images from GitLab Container Registry
+
+To download and run a container from images hosted in GitLab Container Registry,
+use `docker run`:
+
+```
+docker run [options] registry.example.com/group/project [arguments]
+```
+
+For more information on running Docker containers, visit the
+[Docker documentation][docker-docs].
+
+## Control Container Registry from within GitLab
+
+GitLab offers a simple Container Registry management panel. Go to your project
+and click **Registry** in the project menu.
+
+This view will show you all tags in your project and will easily allow you to
+delete them.
+
+![Container Registry panel](img/container_registry_panel.png)
+
+## Build and push images using GitLab CI
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Make sure that your GitLab Runner is configured to allow building Docker images by
+following the [Using Docker Build](../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+
+## Limitations
+
+In order to use a container image from your private project as an `image:` in
+your `.gitlab-ci.yml`, you have to follow the
+[Using a private Docker Registry][private-docker]
+documentation. This workflow will be simplified in the future.
+
+## Troubleshooting the GitLab Container Registry
+
+### Basic Troubleshooting
+
+1. Check to make sure that the system clock on your Docker client and GitLab server have
+   been synchronized (e.g. via NTP).
+
+2. If you are using an S3-backed Registry, double check that the IAM
+   permissions and the S3 credentials (including region) are correct. See [the
+   sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
+   for more details.
+
+3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+   for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
+   there.
+
+### Advanced Troubleshooting
+
+>**NOTE:** The following section is only recommended for experts.
+
+Sometimes it's not obvious what is wrong, and you may need to dive deeper into
+the communication between the Docker client and the Registry to find out
+what's wrong. We will use a concrete example in the past to illustrate how to
+diagnose a problem with the S3 setup.
+
+#### Unexpected 403 error during push
+
+A user attempted to enable an S3-backed Registry. The `docker login` step went
+fine. However, when pushing an image, the output showed:
+
+```
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+dc5e59c14160: Pushing [==================================================>] 14.85 kB
+03c20c1a019a: Pushing [==================================================>] 2.048 kB
+a08f14ef632e: Pushing [==================================================>] 2.048 kB
+228950524c88: Pushing 2.048 kB
+6a8ecde4cc03: Pushing [==>                                                ] 9.901 MB/205.7 MB
+5f70bf18a086: Pushing 1.024 kB
+737f40e80b7f: Waiting
+82b57dbc5385: Waiting
+19429b698a22: Waiting
+9436069b92a3: Waiting
+error parsing HTTP 403 response body: unexpected end of JSON input: ""
+```
+
+This error is ambiguous, as it's not clear whether the 403 is coming from the
+GitLab Rails application, the Docker Registry, or something else. In this
+case, since we know that since the login succeeded, we probably need to look
+at the communication between the client and the Registry.
+
+The REST API between the Docker client and Registry is [described
+here](https://docs.docker.com/registry/spec/api/). Normally, one would just
+use Wireshark or tcpdump to capture the traffic and see where things went
+wrong.  However, since all communication between Docker clients and servers
+are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
+if you know the private key. What can we do instead?
+
+One way would be to disable HTTPS by setting up an [insecure
+Registry](https://docs.docker.com/registry/insecure/). This could introduce a
+security hole and is only recommended for local testing. If you have a
+production system and can't or don't want to do this, there is another way:
+use mitmproxy, which stands for Man-in-the-Middle Proxy.
+
+#### mitmproxy
+
+[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
+client and server to inspect all traffic. One wrinkle is that your system
+needs to trust the mitmproxy SSL certificates for this to work.
+
+The following installation instructions assume you are running Ubuntu:
+
+1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
+1. Run `mitmproxy --port 9000` to generate its certificates.
+   Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
+1. Install the certificate from `~/.mitmproxy` to your system:
+
+    ```sh
+    sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
+    sudo update-ca-certificates
+    ```
+
+If successful, the output should indicate that a certificate was added:
+
+```sh
+Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d....done.
+```
+
+To verify that the certificates are properly installed, run:
+
+```sh
+mitmproxy --port 9000
+```
+
+This will run mitmproxy on port `9000`. In another window, run:
+
+```sh
+curl --proxy http://localhost:9000 https://httpbin.org/status/200
+```
+
+If everything is setup correctly, you will see information on the mitmproxy window and
+no errors from the curl commands.
+
+#### Running the Docker daemon with a proxy
+
+For Docker to connect through a proxy, you must start the Docker daemon with the
+proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
+and then run Docker by hand. As root, run:
+
+```sh
+export HTTP_PROXY="http://localhost:9000"
+export HTTPS_PROXY="https://localhost:9000"
+docker daemon --debug
+```
+
+This will launch the Docker daemon and proxy all connections through mitmproxy.
+
+#### Running the Docker client
+
+Now that we have mitmproxy and Docker running, we can attempt to login and push
+a container image. You may need to run as root to do this. For example:
+
+```sh
+docker login s3-testing.myregistry.com:4567
+docker push s3-testing.myregistry.com:4567/root/docker-test
+```
+
+In the example above, we see the following trace on the mitmproxy window:
+
+![mitmproxy output from Docker](img/mitmproxy-docker.png)
+
+The above image shows:
+
+* The initial PUT requests went through fine with a 201 status code.
+* The 201 redirected the client to the S3 bucket.
+* The HEAD request to the AWS bucket reported a 403 Unauthorized.
+
+What does this mean? This strongly suggests that the S3 user does not have the right
+[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
+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://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
new file mode 100644
index 0000000000000000000000000000000000000000..1892ccabb7044822e466dd05b6413705b8d46815
--- /dev/null
+++ b/doc/user/project/cycle_analytics.md
@@ -0,0 +1,170 @@
+# Cycle Analytics
+
+> [Introduced][ce-5986] in GitLab 8.12.
+>
+> **Note:**
+There are more changes coming to Cycle Analytics, you can follow the following
+issue to track the changes to this feature: [#20975][ce-20975].
+
+Cycle Analytics measures the time it takes to go from an [idea to production] for
+each project you have. This is achieved by not only indicating the total time it
+takes to reach at that point, but the total time is broken down into the
+multiple stages an idea has to pass through to be shipped.
+
+Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+calculates a separate median for each stage.
+
+## Overview
+
+You can find the Cycle Analytics page under your project's **Pipelines > Cycle
+Analytics** tab.
+
+![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
+
+You can see that there are seven stages in total:
+
+- **Issue** (Tracker)
+    - Median time from issue creation until given a milestone or list label
+      (first assignment, any milestone, milestone date or assignee is not required)
+- **Plan** (Board)
+    - Median time from giving an issue a milestone or label until pushing the
+      first commit to the branch
+- **Code** (IDE)
+    - Median time from the first commit to the branch until the merge request is
+      created
+- **Test** (CI)
+    - Median total test time for all commits/merges
+- **Review** (Merge Request/MR)
+    - Median time from merge request creation until the merge request is merged
+      (closed merge requests won't be taken into account)
+- **Staging** (Continuous Deployment)
+    - Median time from when the merge request got merged until the deploy to
+      production (production is last stage/environment)
+- **Production** (Total)
+   - Sum of all the above stages' times excluding the Test (CI) time. To clarify,
+     it's not so much that CI time is "excluded", but rather CI time is already
+     counted in the review stage since CI is done automatically. Most of the
+     other stages are purely sequential, but **Test** is not.
+
+## How the data is measured
+
+Cycle Analytics records cycle time and data based on the project issues with the
+exception of the staging and production stages, where only data deployed to 
+production are measured.
+
+Specifically, if your CI is not set up and you have not defined a `production`
+[environment], then you will not have any data for those stages.
+
+Below you can see in more detail what the various stages of Cycle Analytics mean.
+
+| **Stage** | **Description** |
+| --------- | --------------- |
+| Issue     | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
+| Plan      | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. |
+| Code      | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
+| Test      | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
+| Review    | Measures the median time taken to review the merge request, between its creation and until it's merged. |
+| Staging   | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
+
+---
+
+Here's a little explanation of how this works behind the scenes:
+
+1. Issues and merge requests are grouped together in pairs, such that for each
+   `<issue, merge request>` pair, the merge request has the [issue closing pattern]
+   for the corresponding issue. All other issues and merge requests are **not**
+   considered.
+1. Then the <issue, merge request> pairs are filtered out by last XX days (specified
+   by the UI - default is 90 days). So it prohibits these pairs from being considered.
+1. For the remaining `<issue, merge request>` pairs, we check the information that
+   we need for the stages, like issue creation date, merge request merge time,
+   etc.
+
+To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
+So, if a merge request doesn't close an issue or an issue is not labeled with a
+label present in the Issue Board or assigned a milestone or a project has no
+`production` environment (for staging and production stages), the Cycle Analytics
+dashboard won't present any data at all.
+
+## Example workflow
+
+Below is a simple fictional workflow of a single cycle that happens in a
+single day passing through all seven stages. Note that if a stage does not have
+a start/stop mark, it is not measured and hence not calculated in the median
+time. It is assumed that milestones are created and CI for testing and setting
+environments is configured.
+
+1. Issue is created at 09:00 (start of **Issue** stage).
+1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of
+   **Plan** stage).
+1. Start working on the issue, create a branch locally and make one commit at
+   12:00.
+1. Make a second commit to the branch which mentions the issue number at 12.30
+   (stop of **Plan** stage / start of **Code** stage).
+1. Push branch and create a merge request that contains the [issue closing pattern]
+   in its description at 14:00 (stop of **Code** stage / start of **Test** and
+   **Review** stages).
+1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and
+   takes 5min (stop of **Test** stage).
+1. Review merge request, ensure that everything is OK and merge the merge
+   request at 19:00. (stop of **Review** stage / start of **Staging** stage).
+1. Now that the merge request is merged, a deployment to the `production`
+   environment starts and finishes at 19:30 (stop of **Staging** stage).
+1. The cycle completes and the sum of the median times of the previous stages
+   is recorded to the **Production** stage. That is the time between creating an
+   issue and deploying its relevant merge request to production.
+
+From the above example you can conclude the time it took each stage to complete
+as long as their total time:
+
+- **Issue**:  2h (11:00 - 09:00)
+- **Plan**:   1h (12:00 - 11:00)
+- **Code**:   2h (14:00 - 12:00)
+- **Test**:   5min
+- **Review**: 5h (19:00 - 14:00)
+- **Staging**:  30min (19:30 - 19:00)
+- **Production**: Since this stage measures the sum of median time off all
+  previous stages, we cannot calculate it if we don't know the status of the
+  stages before. In case this is the very first cycle that is run in the project,
+  then the **Production** time is 10h 30min (19:30 - 09:00)
+
+A few notes:
+
+- In the above example we demonstrated that it doesn't matter if your first
+  commit doesn't mention the issue number, you can do this later in any commit
+  of the branch you are working on.
+- You can see that the **Test** stage is not calculated to the overall time of
+  the cycle since it is included in the **Review** process (every MR should be
+  tested).
+- The example above was just **one cycle** of the seven stages. Add multiple
+  cycles, calculate their median time and the result is what the dashboard of
+  Cycle Analytics is showing.
+
+## Permissions
+
+The current permissions on the Cycle Analytics dashboard are:
+
+- Public projects - anyone can access
+- Private/internal projects - any member (guest level and above) can access
+
+You can [read more about permissions][permissions] in general.
+
+## More resources
+
+Learn more about Cycle Analytics in the following resources:
+
+- [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/)
+- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
+- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+
+
+[board]: issue_board.md#creating-a-new-list
+[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
+[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
+[environment]: ../../ci/yaml/README.md#environment
+[GitLab flow]: ../../workflow/gitlab_flow.md
+[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
+[issue closing pattern]: issues/automatic_issue_closing.md
+[permissions]: ../permissions.md
+[yml]: ../../ci/yaml/README.md
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea7496af089f5cfcd1e4a41aea702e1e2c6d0ca6
--- /dev/null
+++ b/doc/user/project/description_templates.md
@@ -0,0 +1,42 @@
+# Description templates
+
+>[Introduced][ce-4981] in GitLab 8.11.
+
+Description templates allow you to define context-specific templates for issue
+and merge request description fields for your project.
+
+## Overview
+
+By using the description templates, users that create a new issue or merge
+request can select a description template to help them communicate with other
+contributors effectively.
+
+Every GitLab project can define its own set of description templates as they
+are added to the root directory of a GitLab project's repository.
+
+Description templates must be written in [Markdown](../markdown.md) and stored
+in your project's repository under a directory named `.gitlab`. Only the
+templates of the default branch will be taken into account.
+
+## Creating issue templates
+
+Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
+directory in your repository. Commit and push to your default branch.
+
+## Creating merge request templates
+
+Similarly to issue templates, create a new Markdown (`.md`) file inside the
+`.gitlab/merge_request_templates/` directory in your repository. Commit and
+push to your default branch.
+
+## Using the templates
+
+Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
+This will enable the `Bug` dropdown option when creating or editing issues. When
+`Bug` is selected, the content from the `Bug.md` template file will be copied
+to the issue description field. The 'Reset template' button will discard any
+changes you made after picking the template and return it to its initial status.
+
+![Description templates](img/description_templates.png)
+
+[ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
new file mode 100644
index 0000000000000000000000000000000000000000..21ef94e61f76e950ec002c84a031badbf3a55696
--- /dev/null
+++ b/doc/user/project/git_attributes.md
@@ -0,0 +1,22 @@
+# Git Attributes
+
+GitLab supports defining custom [Git attributes][gitattributes] such as what
+files to treat as binary, and what language to use for syntax highlighting
+diffs.
+
+To define these attributes, create a file called `.gitattributes` in the root
+directory of your repository and push it to the default branch of your project.
+
+## Encoding Requirements
+
+The `.gitattributes` file _must_ be encoded in UTF-8 and _must not_ contain a
+Byte Order Mark. If a different encoding is used, the file's contents will be
+ignored.
+
+## Syntax Highlighting
+
+The `.gitattributes` file can be used to define which language to use when
+syntax highlighting files and diffs. See ["Syntax
+Highlighting"](highlighting.md) for more information.
+
+[gitattributes]: https://git-scm.com/docs/gitattributes
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fffa2a91d8f4d4cbdc16e7a545afb2562e913af
Binary files /dev/null and b/doc/user/project/img/container_registry_enable.png differ
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
new file mode 100644
index 0000000000000000000000000000000000000000..60fd76192b780c24841d0c9177a5be841b148e6b
Binary files /dev/null and b/doc/user/project/img/container_registry_panel.png differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
new file mode 100644
index 0000000000000000000000000000000000000000..36b883aaa97211a7ab2fed1e34f5525599f60348
Binary files /dev/null and b/doc/user/project/img/container_registry_tab.png differ
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..b212134d5ed91a2613504ae2790a53162e6c51ef
Binary files /dev/null and b/doc/user/project/img/cycle_analytics_landing_page.png differ
diff --git a/doc/user/project/img/description_templates.png b/doc/user/project/img/description_templates.png
new file mode 100644
index 0000000000000000000000000000000000000000..c41cc77a94c0e888f97e376603f45e6c3feb24ad
Binary files /dev/null and b/doc/user/project/img/description_templates.png differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
new file mode 100644
index 0000000000000000000000000000000000000000..63c269f6dbc41a8f9d8db31f485ae95f012473c5
Binary files /dev/null and b/doc/user/project/img/issue_board.png differ
diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b8c10eaa0a4d9c8b58614ff233dcd5c552eea75
Binary files /dev/null and b/doc/user/project/img/issue_board_add_list.png differ
diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png
new file mode 100644
index 0000000000000000000000000000000000000000..112ea17153908c157028c255c5adcb854069c51b
Binary files /dev/null and b/doc/user/project/img/issue_board_search_backlog.png differ
diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png
new file mode 100644
index 0000000000000000000000000000000000000000..b69ef034954effa4d2f921e633fbe733fb290ee5
Binary files /dev/null and b/doc/user/project/img/issue_board_system_notes.png differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
new file mode 100644
index 0000000000000000000000000000000000000000..b757faeb230ea8e1ec12bf546834e249fbc85617
Binary files /dev/null and b/doc/user/project/img/issue_board_welcome_message.png differ
diff --git a/doc/user/project/img/koding_build-in-progress.png b/doc/user/project/img/koding_build-in-progress.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8cc81834c4efffa0c2f8db212c931aa3b8b48fd
Binary files /dev/null and b/doc/user/project/img/koding_build-in-progress.png differ
diff --git a/doc/user/project/img/koding_build-logs.png b/doc/user/project/img/koding_build-logs.png
new file mode 100644
index 0000000000000000000000000000000000000000..a04cd5aff9926d86d8d36cc4345d59765a91240c
Binary files /dev/null and b/doc/user/project/img/koding_build-logs.png differ
diff --git a/doc/user/project/img/koding_build-success.png b/doc/user/project/img/koding_build-success.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a0dd296480b7f81b297cc79936873ded03e6d60
Binary files /dev/null and b/doc/user/project/img/koding_build-success.png differ
diff --git a/doc/user/project/img/koding_commit-koding.yml.png b/doc/user/project/img/koding_commit-koding.yml.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e133c50327cd620ae9738d51fc159812d0fb49c
Binary files /dev/null and b/doc/user/project/img/koding_commit-koding.yml.png differ
diff --git a/doc/user/project/img/koding_different-stack-on-mr-try.png b/doc/user/project/img/koding_different-stack-on-mr-try.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd25e32f64847e94c7efe9a4210bb03564e4bf6b
Binary files /dev/null and b/doc/user/project/img/koding_different-stack-on-mr-try.png differ
diff --git a/doc/user/project/img/koding_edit-on-ide.png b/doc/user/project/img/koding_edit-on-ide.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd5aaff75f504a257090ac2d3f7e948bc7aea12e
Binary files /dev/null and b/doc/user/project/img/koding_edit-on-ide.png differ
diff --git a/doc/user/project/img/koding_enable-koding.png b/doc/user/project/img/koding_enable-koding.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0ae0ee99186029119f9025be23d3d34099b7404
Binary files /dev/null and b/doc/user/project/img/koding_enable-koding.png differ
diff --git a/doc/user/project/img/koding_landing.png b/doc/user/project/img/koding_landing.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c629d9b05e6ba1a2f0e6959508d6f0f391a1c26
Binary files /dev/null and b/doc/user/project/img/koding_landing.png differ
diff --git a/doc/user/project/img/koding_open-gitlab-from-koding.png b/doc/user/project/img/koding_open-gitlab-from-koding.png
new file mode 100644
index 0000000000000000000000000000000000000000..c958cf8f22423c9a5044f6bba39f22f844046d00
Binary files /dev/null and b/doc/user/project/img/koding_open-gitlab-from-koding.png differ
diff --git a/doc/user/project/img/koding_run-in-ide.png b/doc/user/project/img/koding_run-in-ide.png
new file mode 100644
index 0000000000000000000000000000000000000000..f91ee0f74cc1d875c300c8f209deae437b96af77
Binary files /dev/null and b/doc/user/project/img/koding_run-in-ide.png differ
diff --git a/doc/user/project/img/koding_run-mr-in-ide.png b/doc/user/project/img/koding_run-mr-in-ide.png
new file mode 100644
index 0000000000000000000000000000000000000000..502817a2a46528752ab391761305316859beffb9
Binary files /dev/null and b/doc/user/project/img/koding_run-mr-in-ide.png differ
diff --git a/doc/user/project/img/koding_set-up-ide.png b/doc/user/project/img/koding_set-up-ide.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f408c980b51b3340561e7aff24fb21671e540a6
Binary files /dev/null and b/doc/user/project/img/koding_set-up-ide.png differ
diff --git a/doc/user/project/img/koding_stack-import.png b/doc/user/project/img/koding_stack-import.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a4e3c87fc855fd33e761b0e1ed7d57098d21ffe
Binary files /dev/null and b/doc/user/project/img/koding_stack-import.png differ
diff --git a/doc/user/project/img/koding_start-build.png b/doc/user/project/img/koding_start-build.png
new file mode 100644
index 0000000000000000000000000000000000000000..52159440f62883217f430394b2d0113d3f4dee8f
Binary files /dev/null and b/doc/user/project/img/koding_start-build.png differ
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png
similarity index 100%
rename from doc/container_registry/img/mitmproxy-docker.png
rename to doc/user/project/img/mitmproxy-docker.png
diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png
index 57ca2ac5f9e56fceb89864c5074186fec562ded4..cd9f5c00eea85022dcbda9f8d847ff0c0ec49023 100644
Binary files a/doc/user/project/img/project_settings_list.png and b/doc/user/project/img/project_settings_list.png differ
diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png
index 9c33db36586752a18cf9575fcf6c5385c3d2d1bd..812cc8767b7262f06d76e86509a23c6d0d31a3c1 100644
Binary files a/doc/user/project/img/protected_branches_devs_can_push.png and b/doc/user/project/img/protected_branches_devs_can_push.png differ
diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png
index 9f070f7a208d94b6890227d7801f18c944d5f2e6..f33f1b2bdb618c2172bcc3a01010933bf34182d7 100644
Binary files a/doc/user/project/img/protected_branches_list.png and b/doc/user/project/img/protected_branches_list.png differ
diff --git a/doc/user/project/img/protected_branches_page.png b/doc/user/project/img/protected_branches_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..1585dde5b29596cf24d0c34aa506b1c40410b28e
Binary files /dev/null and b/doc/user/project/img/protected_branches_page.png differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
new file mode 100644
index 0000000000000000000000000000000000000000..4a6c0d8824142c6758c45802b3c22c7c3c32168c
--- /dev/null
+++ b/doc/user/project/issue_board.md
@@ -0,0 +1,188 @@
+# Issue board
+
+> [Introduced][ce-5554] in GitLab 8.11.
+
+The GitLab Issue Board is a software project management tool used to plan,
+organize, and visualize a workflow for a feature or product release.
+It can be seen like a light version of a [Kanban] or a [Scrum] board.
+
+Other interesting links:
+
+- [GitLab Issue Board landing page on about.gitlab.com][landing]
+- [YouTube video introduction to Issue Boards][youtube]
+
+## Overview
+
+The Issue Board builds on GitLab's existing issue tracking functionality and
+leverages the power of [labels] by utilizing them as lists of the scrum board.
+
+With the Issue Board you can have a different view of your issues while also
+maintaining the same filtering and sorting abilities you see across the
+issue tracker.
+
+Below is a table of the definitions used for GitLab's Issue Board.
+
+| What we call it  | What it means |
+| --------------  | ------------- |
+| **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). |
+
+There are three types of lists, the ones you create based on your labels, and
+two default:
+
+- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues. Always appears on the very right.
+Label list: a list based on a label. It shows all issues with that label.
+- Label list: a list based on a label. It shows all opened issues with that label.
+
+![GitLab Issue Board](img/issue_board.png)
+
+---
+
+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.
+- 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.
+- Create a new list from a non-existing label by [creating the label on-the-fly](#creating-a-new-list)
+  within the Issue Board.
+- [Filter issues](#filtering-issues) that appear across your Issue Board.
+
+If you are not able to perform one or more of the things above, make sure you
+have the right [permissions](#permissions).
+
+## First time using the Issue Board
+
+The first time you navigate to your Issue Board, you will be presented with the
+two default lists (**Backlog** and **Done**) and a welcoming message that gives
+you two options. You can either create a predefined set of labels and create
+their corresponding lists to the Issue Board or opt-out and use your own lists.
+
+![Issue Board welcome message](img/issue_board_welcome_message.png)
+
+If you choose to use and create the predefined lists, they will appear as empty
+because the labels associated to them will not exist up until that moment,
+which means the system has no way of populating them automatically. That's of
+course if the predefined labels don't already exist. If any of them does exist,
+the list will be created and filled with the issues that have that label.
+
+## Creating a new list
+
+Create a new list by clicking on the **Create new list** button at the upper
+right corner of the Issue Board.
+
+![Issue Board welcome message](img/issue_board_add_list.png)
+
+Simply choose the label to create the list from. The new list will be inserted
+at the end of the lists, before **Done**. Moving and reordering lists is as
+easy as dragging them around.
+
+To create a list for a label that doesn't yet exist, simply create the label by
+choosing **Create new label**. The label will be created on-the-fly and it will
+be immediately added to the dropdown. You can now choose it to create a list.
+
+## Deleting a list
+
+To delete a list from the Issue Board use the small trash icon that is present
+in the list's heading. A confirmation dialog will appear for you to confirm.
+
+Deleting a list doesn't have any effect in issues and labels, it's just the
+list view that is removed. You can always add it back later if you need.
+
+## Searching issues in the Backlog list
+
+The very first time you start using the Issue Board, it is very likely your
+issue tracker is already populated with labels and issues. In that case,
+**Backlog** will have all the issues that don't belong to another list, and
+**Done** will have all the closed ones.
+
+For performance and visibility reasons, each list shows the first 20 issues
+by default. If you have more than 20, you have to start scrolling down for the
+next 20 issues to appear. This can be cumbersome if your issue tracker hosts
+hundreds of issues, and for that reason it is easier to search for issues to
+move from **Backlog** to another list.
+
+Start typing in the search bar under the **Backlog** list and the relevant
+issues will appear.
+
+![Issue Board search Backlog](img/issue_board_search_backlog.png)
+
+## Filtering issues
+
+You should be able to use the filters on top of your Issue Board to show only
+the results you want. This is similar to the filtering used in the issue tracker
+since the metadata from the issues and labels are re-used in the Issue Board.
+
+You can filter by author, assignee, milestone and label.
+
+## Creating workflows
+
+By reordering your lists, you can create workflows. As lists in Issue Boards are
+based on labels, it works out of the box with your existing issues. So if you've
+already labeled things with 'Backend' and 'Frontend', the issue will appear in
+the lists as you create them. In addition, this means you can easily move
+something between lists by changing a label.
+
+A typical workflow of using the Issue Board would be:
+
+1. You have [created][create-labels] and [prioritized][label-priority] labels
+   so that you can easily categorize your issues.
+1. You have a bunch of issues (ideally labeled).
+1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
+   create a workflow.
+1. You move issues around in lists so that your team knows who should be working
+   on what issue.
+1. When the work by one team is done, the issue can be dragged to the next list
+   so someone else can pick up.
+1. When the issue is finally resolved, the issue is moved to the **Done** list
+   and gets automatically closed.
+
+For instance you can create a list based on the label of 'Frontend' and one for
+'Backend'. A designer can start working on an issue by dragging it from
+**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being
+worked on by the designers. Then, once they're done, all they have to do is
+drag it over to the next list, 'Backend', where a backend developer can
+eventually pick it up. Once they’re done, they move it to **Done**, to close the
+issue.
+
+This process can be seen clearly when visiting an issue since with every move
+to another list the label changes and a system not is recorded.
+
+![Issue Board system notes](img/issue_board_system_notes.png)
+
+## Permissions
+
+[Developers and up](../permissions.md) can use all the functionality of the
+Issue Board, that is create/delete lists and drag issues around.
+
+## Tips
+
+A few things to remember:
+
+- The label that corresponds to a list is hidden for issues under that list.
+- Moving an issue between lists removes the label from the list it came from
+  and adds the label from the list it goes to.
+- When moving a card to **Done**, the label of the list it came from is removed
+  and the issue gets closed.
+- An issue can exist in multiple lists if it has more than one label.
+- Lists are populated with issues automatically if the issues are labeled.
+- 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.
+
+[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554
+[labels]: ./labels.md
+[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development)
+[kanban]: https://en.wikipedia.org/wiki/Kanban_(development)
+[create-labels]: ./labels.md#create-new-labels
+[label-priority]: ./labels.md#prioritize-labels
+[landing]: https://about.gitlab.com/solutions/issueboard
+[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6f3a7d5555f5e444ab5806a958b0adebd9321f1
--- /dev/null
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -0,0 +1,55 @@
+# Automatic issue closing
+
+>**Note:**
+This is the user docs. In order to change the default issue closing pattern,
+follow the steps in the [administration docs].
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+If a commit message or merge request description contains a sentence matching
+a certain regular expression, all issues referenced from the matched text will
+be closed. This happens when the commit is pushed to a project's **default**
+branch, or when a commit or merge request is merged into it.
+
+## Default closing pattern value
+
+When not specified, the default issue closing pattern as shown below will be
+used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+```
+
+Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
+source code that can match a reference to 1) a local issue (`#123`),
+2) a cross-project issue (`group/project#123`) or 3) a link to an issue
+(`https://gitlab.example.com/group/project/issues/123`).
+
+---
+
+This translates to the following keywords:
+
+- Close, Closes, Closed, Closing, close, closes, closed, closing
+- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
+- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+
+---
+
+For example the following commit message:
+
+```
+Awesome commit message
+
+Fix #20, Fixes #21 and Closes group/otherproject#22.
+This commit is also related to #17 and fixes #18, #19
+and https://gitlab.example.com/group/otherproject/issues/23.
+```
+
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed
+to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as
+it does not match the pattern. It works with multi-line commit messages as well
+as one-liners when used with `git commit -m`.
+
+[administration docs]: ../../../administration/issue_closing_pattern.md
diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md
new file mode 100644
index 0000000000000000000000000000000000000000..c56a1efe3c225d18139f811abed373b6fb8a3afb
--- /dev/null
+++ b/doc/user/project/koding.md
@@ -0,0 +1,128 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through using Koding integration on GitLab in
+detail. For configuring and installing please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+You can use Koding integration to run and develop your projects on GitLab. This
+will allow you and the users to test your project without leaving the browser.
+Koding handles projects as stacks which are basic recipes to define your
+environment for your project. With this integration you can automatically
+create a proper stack template for your projects. Currently auto-generated
+stack templates are designed to work with AWS which requires a valid AWS
+credential to be able to use these stacks. You can find more information about
+stacks and the other providers that you can use on Koding following the
+[Koding documentation][koding-docs].
+
+## Enable Integration
+
+You can enable Koding integration by providing the running Koding instance URL
+in Application Settings under **Admin area > Settings** (`/admin/application_settings`).
+
+![Enable Koding](img/koding_enable-koding.png)
+
+Once enabled you will see `Koding` link on your sidebar which leads you to
+Koding Landing page.
+
+![Koding Landing](img/koding_landing.png)
+
+You can navigate to running Koding instance from here. For more information and
+details about configuring the integration, please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+## Set up Koding on Projects
+
+Once it's enabled, you will see some integration buttons on Project pages,
+Merge Requests etc. To get started working on a specific project you first need
+to create a `.koding.yml` file under your project root. You can easily do that
+by using `Set Up Koding` button which will be visible on every project's
+landing page;
+
+![Set Up Koding](img/koding_set-up-ide.png)
+
+Once you click this will open a New File page on GitLab with auto-generated
+`.koding.yml` content based on your server and repository configuration.
+
+![Commit .koding.yml](img/koding_commit-koding.yml.png)
+
+
+## Run a project on Koding
+
+If there is `.koding.yml` exists in your project root, you will see
+`Run in IDE (Koding)` button in your project landing page. You can initiate the
+process from here.
+
+![Run on Koding](img/koding_run-in-ide.png)
+
+This will open Koding defined in the settings in a new window and will start
+importing the project's stack file.
+
+![Import Stack](img/koding_stack-import.png)
+
+You should see the details of your repository imported into your Koding
+instance. Once it's completed it will lead you to the Stack Editor and from
+there you can start using your new stack integrated with your project on your
+GitLab instance. For details about what's next you can follow
+[this guide](https://www.koding.com/docs/creating-an-aws-stack) from step 8.
+
+Once stack initialized you will see the `README.md` content from your project
+in `Stack Build` wizard, this wizard will let you build the stack and import
+your project into it. **Once it's completed it will automatically open the
+related vm instead of importing from scratch**.
+
+![Stack Building](img/koding_start-build.png)
+
+This will take time depending on the required environment.
+
+![Stack Building in Progress](img/koding_build-in-progress.png)
+
+It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
+AWS region. (`t2.nano` is default vm type on auto-generated stack template
+which can be manually changed).
+
+![Stack Building Success](img/koding_build-success.png)
+
+You can check out the `Build Logs` from this success modal as well.
+
+![Stack Build Logs](img/koding_build-logs.png)
+
+You can now `Start Coding`!
+
+![Edit On IDE](img/koding_edit-on-ide.png)
+
+## Try a Merge Request on IDE
+
+It's also possible to try a change on IDE before merging it. This flow only
+enabled if the target project has `.koding.yml` in it's target branch. You
+should see the alternative version of `Run in IDE (Koding)` button in merge
+request pages as well;
+
+![Run in IDE on MR](img/koding_run-mr-in-ide.png)
+
+This will again take you to Koding with proper arguments passed, which will
+allow Koding to modify the stack template provided by target branch. You can
+see the difference;
+
+![Different Branch for MR](img/koding_different-stack-on-mr-try.png)
+
+The flow for the branch stack is also same with the regular project flow.
+
+## Open GitLab from Koding
+
+Since stacks generated with import flow defined in previous steps, they have
+information about the repository they are belonging to. By using this
+information you can access to related GitLab page from stacks on your sidebar
+on Koding.
+
+![Open GitLab from Koding](img/koding_open-gitlab-from-koding.png)
+
+## Other links
+
+- [YouTube video on GitLab + Koding workflow][youtube]
+- [Koding documentation][koding-docs]
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
+[youtube]: https://youtu.be/3wei5yv_Ye8
+[koding-docs]: https://www.koding.com/docs
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 0f7e9eede19ab54a785ce62cb519a06385dd9b57..cf1d9cbe69cc7cd429081802d4edc9411844cf55 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -1,8 +1,8 @@
 # Labels
 
 Labels provide an easy way to categorize the issues or merge requests based on
-descriptive titles like `bug`, `documentation` or any other text you feel like
-it. They can have different colors, a description, and are visible throughout
+descriptive titles like `bug`, `documentation` or any other text you feel like.
+They can have different colors, a description, and are visible throughout
 the issue tracker or inside each issue individually.
 
 With labels, you can navigate the issue tracker and filter any bloated
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
new file mode 100644
index 0000000000000000000000000000000000000000..5af9a5d049c0518c81ba6007073a8335a1d2f9ba
--- /dev/null
+++ b/doc/user/project/merge_requests.md
@@ -0,0 +1,169 @@
+# Merge Requests
+
+Merge requests allow you to exchange changes you made to source code and
+collaborate with other people on the same project.
+
+## Authorization for merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches][] in a single repository
+1. Working with forks of an authoritative project
+
+[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
+
+## Cherry-pick changes
+
+Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
+in a merged merge requests or a commit.
+
+[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
+
+## Merge when build succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when all
+builds succeed. This way, you don't have to wait for the builds to finish and
+remember to merge the request manually.
+
+[Learn more about merging when build succeeds.](merge_requests/merge_when_build_succeeds.md)
+
+## Resolve discussion comments in merge requests reviews
+
+Keep track of the progress during a code review with resolving comments.
+Resolving comments prevents you from forgetting to address feedback and lets
+you hide discussions that are no longer relevant.
+
+[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
+
+## Resolve conflicts
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI.
+
+[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
+
+## Revert changes
+
+GitLab implements Git's powerful feature to revert any commit with introducing
+a **Revert** button in merge requests and commit details.
+
+[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
+
+## Merge requests versions
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+[Read more about the merge requests versions.](merge_requests/versions.md)
+
+## Work In Progress merge requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked as a **Work In Progress**.
+
+[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
+
+## Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](merge_requests/img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
+#### Checkout locally by adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+    mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any
+remote. For example, to check out the merge request with ID 5 as shown in GitLab
+from the `upstream` remote, do:
+
+```
+git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check
+it out.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+  url = https://gitlab.com/gitlab-org/gitlab-ce.git
+  fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+  url = https://gitlab.com/gitlab-org/gitlab-ce.git
+  fetch = +refs/heads/*:refs/remotes/origin/*
+  fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-ce.git
+ * [new ref]         refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref]         refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+[protected branches]: protected_branches.md
diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md
new file mode 100644
index 0000000000000000000000000000000000000000..59b3fe7242cb177c4d85dbe5517e879752a1d28f
--- /dev/null
+++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md
@@ -0,0 +1,56 @@
+# Authorization for Merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches] in a single repository.
+1. Working with forks of an authoritative project.
+
+## Protected branch flow
+
+With the protected branch flow everybody works within the same GitLab project.
+
+The project maintainers get Master access and the regular developers get
+Developer access.
+
+The maintainers mark the authoritative branches as 'Protected'.
+
+The developers push feature branches to the project and create merge requests
+to have their feature branches reviewed and merged into one of the protected
+branches.
+
+By default, only users with Master access can merge changes into a protected
+branch.
+
+**Advantages**
+
+- Fewer projects means less clutter.
+- Developers need to consider only one remote repository.
+
+**Disadvantages**
+
+- Manual setup of protected branch required for each new project
+
+## Forking workflow
+
+With the forking workflow the maintainers get Master access and the regular
+developers get Reporter access to the authoritative repository, which prohibits
+them from pushing any changes to it.
+
+Developers create forks of the authoritative project and push their feature
+branches to their own forks.
+
+To get their changes into master they need to create a merge request across
+forks.
+
+**Advantages**
+
+- In an appropriately configured GitLab group, new projects automatically get
+  the required access restrictions for regular developers: fewer manual steps
+  to configure authorization for new projects.
+
+**Disadvantages**
+
+- The project need to keep their forks up to date, which requires more advanced
+  Git skills (managing multiple remotes).
+
+[protected branches]: ../protected_branches.md
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
new file mode 100644
index 0000000000000000000000000000000000000000..64b94d810242317c9f6c70827ed527ab4ca39767
--- /dev/null
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -0,0 +1,52 @@
+# Cherry-pick changes
+
+> [Introduced][ce-3514] in GitLab 8.7.
+
+---
+
+GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+with introducing a **Cherry-pick** button in Merge Requests and commit details.
+
+## Cherry-picking a Merge Request
+
+After the Merge Request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that Merge Request:
+
+![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
+
+---
+
+You can cherry-pick the changes directly into the selected branch or you can opt to
+create a new Merge Request with the cherry-pick changes:
+
+![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+
+## Cherry-picking a Commit
+
+You can cherry-pick a Commit from the Commit details page:
+
+![Cherry-pick commit](img/cherry_pick_changes_commit.png)
+
+---
+
+Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
+directly into the target branch or create a new Merge Request to cherry-pick the
+changes:
+
+![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
+
+---
+
+Please note that when cherry-picking merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to cherry-pick a merge commit using the second parent as the
+mainline:
+
+```bash
+git cherry-pick -m 2 7a39eb0
+```
+
+[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
+[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
similarity index 100%
rename from doc/workflow/img/cherry_pick_changes_commit.png
rename to doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
similarity index 100%
rename from doc/workflow/img/cherry_pick_changes_commit_modal.png
rename to doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
similarity index 100%
rename from doc/workflow/img/cherry_pick_changes_mr.png
rename to doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
similarity index 100%
rename from doc/workflow/img/cherry_pick_changes_mr_modal.png
rename to doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png
similarity index 100%
rename from doc/workflow/merge_requests/commit_compare.png
rename to doc/user/project/merge_requests/img/commit_compare.png
diff --git a/doc/user/project/merge_requests/img/conflict_section.png b/doc/user/project/merge_requests/img/conflict_section.png
new file mode 100644
index 0000000000000000000000000000000000000000..842e50b14b2c6646139e7d92ccb68b39a7610787
Binary files /dev/null and b/doc/user/project/merge_requests/img/conflict_section.png differ
diff --git a/doc/user/project/merge_requests/img/discussion_view.png b/doc/user/project/merge_requests/img/discussion_view.png
new file mode 100644
index 0000000000000000000000000000000000000000..83bb60acce2f947730e3add6f0b0a801dcbf40de
Binary files /dev/null and b/doc/user/project/merge_requests/img/discussion_view.png differ
diff --git a/doc/user/project/merge_requests/img/discussions_resolved.png b/doc/user/project/merge_requests/img/discussions_resolved.png
new file mode 100644
index 0000000000000000000000000000000000000000..85428129ac8858fef27d082b39bec420dceae02f
Binary files /dev/null and b/doc/user/project/merge_requests/img/discussions_resolved.png differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..06ee4908edca501c4eb0609ab6ac7517badc4c9f
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_request_diff.png differ
diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffb96b17b0740373838cea9d446435977d443147
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_request_widget.png differ
diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
similarity index 100%
rename from doc/workflow/merge_when_build_succeeds/enable.png
rename to doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
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_build_succeeds_only_if_succeeds_msg.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b9756b74183abbefeac224d0361b84bce1547db
Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png differ
diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
similarity index 100%
rename from doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
rename to doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
similarity index 100%
rename from doc/workflow/merge_when_build_succeeds/status.png
rename to doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png
new file mode 100644
index 0000000000000000000000000000000000000000..52c8acf15e02e51caf7df99fb5febaeb054162e2
Binary files /dev/null and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png differ
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
new file mode 100644
index 0000000000000000000000000000000000000000..79ba5c362c7543a0bb83e99e753434333f29bc13
Binary files /dev/null and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png differ
diff --git a/doc/user/project/merge_requests/img/resolve_comment_button.png b/doc/user/project/merge_requests/img/resolve_comment_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c4ab2f5d531faed671ba8dee2980430e12b739f
Binary files /dev/null and b/doc/user/project/merge_requests/img/resolve_comment_button.png differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_button.png b/doc/user/project/merge_requests/img/resolve_discussion_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..73f265bb1019dc39257b5ebdda7c352df23b4ec1
Binary files /dev/null and b/doc/user/project/merge_requests/img/resolve_discussion_button.png differ
diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png
similarity index 100%
rename from doc/workflow/img/revert_changes_commit.png
rename to doc/user/project/merge_requests/img/revert_changes_commit.png
diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
similarity index 100%
rename from doc/workflow/img/revert_changes_commit_modal.png
rename to doc/user/project/merge_requests/img/revert_changes_commit_modal.png
diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png
similarity index 100%
rename from doc/workflow/img/revert_changes_mr.png
rename to doc/user/project/merge_requests/img/revert_changes_mr.png
diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
similarity index 100%
rename from doc/workflow/img/revert_changes_mr_modal.png
rename to doc/user/project/merge_requests/img/revert_changes_mr_modal.png
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c86f2c68ac59643f47938afa344f00c5bb50f18
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions.png differ
diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png
new file mode 100644
index 0000000000000000000000000000000000000000..890cae7768cc9fc0814855963ffd9d914ad4c577
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions_compare.png differ
diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bab9304e147aa4bbc7fafd022bd18e78ecffa72
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions_dropdown.png differ
diff --git a/doc/user/project/merge_requests/img/versions_system_note.png b/doc/user/project/merge_requests/img/versions_system_note.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c9d7715745c3e437dd7528fbe04cb3e8b40c011
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions_system_note.png differ
diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
similarity index 100%
rename from doc/workflow/wip_merge_requests/blocked_accept_button.png
rename to doc/user/project/merge_requests/img/wip_blocked_accept_button.png
diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
similarity index 100%
rename from doc/workflow/wip_merge_requests/mark_as_wip.png
rename to doc/user/project/merge_requests/img/wip_mark_as_wip.png
diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
similarity index 100%
rename from doc/workflow/wip_merge_requests/unmark_as_wip.png
rename to doc/user/project/merge_requests/img/wip_unmark_as_wip.png
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
new file mode 100644
index 0000000000000000000000000000000000000000..285b1798ac55db2b6d0918d8afa6e8f1692cd329
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -0,0 +1,58 @@
+# Merge Request discussion resolution
+
+> [Introduced][ce-5022] in GitLab 8.11.
+
+Discussion resolution helps keep track of progress during code review.
+Resolving comments prevents you from forgetting to address feedback and lets you
+hide discussions that are no longer relevant.
+
+!["A discussion between two people on a piece of code"][discussion-view]
+
+Comments and discussions can be resolved by anyone with at least Developer
+access to the project, as well as by the author of the merge request.
+
+## Marking a comment or discussion as resolved
+
+You can mark a discussion as resolved by clicking the "Resolve discussion"
+button at the bottom of the discussion.
+
+!["Resolve discussion" button][resolve-discussion-button]
+
+Alternatively, you can mark each comment as resolved individually.
+
+!["Resolve comment" button][resolve-comment-button]
+
+## Jumping between unresolved discussions
+
+When a merge request has a large number of comments it can be difficult to track
+what remains unresolved. You can jump between unresolved discussions with the
+Jump button next to the Reply field on a discussion.
+
+You can also jump to the first unresolved discussion from the button next to the
+resolved discussions tracker.
+
+!["3/4 discussions resolved"][discussions-resolved]
+
+## Only allow merge requests to be merged if all discussions are resolved
+
+> [Introduced][ce-7125] in GitLab 8.14.
+
+You can prevent merge requests from being merged until all discussions are resolved.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if all discussions are resolved** check
+box and hit **Save** for the changes to take effect.
+
+![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png)
+
+From now on, you will not be able to merge from the UI until all discussions
+are resolved.
+
+![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
+
+[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
+[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
+[resolve-discussion-button]: img/resolve_discussion_button.png
+[resolve-comment-button]: img/resolve_comment_button.png
+[discussion-view]: img/discussion_view.png
+[discussions-resolved]: img/discussions_resolved.png
diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md
new file mode 100644
index 0000000000000000000000000000000000000000..d4e5b5de6857d4bcc16631a41ee797d9a65092e1
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -0,0 +1,46 @@
+# Merge When Build Succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when the
+builds pipeline succeed. This way, you don't have to wait for the builds to
+finish and remember to merge the request manually.
+
+![Enable](img/merge_when_build_succeeds_enable.png)
+
+When you hit the "Merge When Build Succeeds" button, the status of the merge
+request will be updated to represent the impending merge. If you cannot wait
+for the pipeline to succeed and want to merge immediately, this option is
+available in the dropdown menu on the right of the main button.
+
+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)
+
+When the pipeline succeeds, the merge request will automatically be merged.
+When the pipeline fails, the author gets a chance to retry any failed builds,
+or to push new commits to fix the failure.
+
+When the builds are retried and succeed on the second try, the merge request
+will automatically be merged after all. When the merge request is updated with
+new commits, the automatic merge is automatically canceled to allow the new
+changes to be reviewed.
+
+## Only allow merge requests to be merged if the build succeeds
+
+> **Note:**
+You need to have builds configured to enable this feature.
+
+You can prevent merge requests from being merged if their build did not succeed.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if the build succeeds** check box and
+hit **Save** for the changes to take effect.
+
+![Only allow merge if build succeeds settings](img/merge_when_build_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 builds pass.
+
+![Only allow merge if build succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
new file mode 100644
index 0000000000000000000000000000000000000000..4d7225bd820dd7ac334cd40d7425b30b2ddc2cc6
--- /dev/null
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -0,0 +1,42 @@
+# Merge conflict resolution
+
+> [Introduced][ce-5479] in GitLab 8.11.
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI. (See
+[conflicts available for resolution](#conflicts-available-for-resolution) for
+more information on when this is available.) If this is an option, you will see
+a **resolve these conflicts** link in the merge request widget:
+
+![Merge request widget](img/merge_request_widget.png)
+
+Clicking this will show a list of files with conflicts, with conflict sections
+highlighted:
+
+![Conflict section](img/conflict_section.png)
+
+Once all conflicts have been marked as using 'ours' or 'theirs', the conflict
+can be resolved. This will perform a merge of the target branch of the merge
+request into the source branch, resolving the conflicts using the options
+chosen. If the source branch is `feature` and the target branch is `master`,
+this is similar to performing `git checkout feature; git merge master` locally.
+
+## Conflicts available for resolution
+
+GitLab allows resolving conflicts in a file where all of the below are true:
+
+- The file is text, not binary
+- The file is in a UTF-8 compatible encoding
+- The file does not already contain conflict markers
+- The file, with conflict markers added, is not over 200 KB in size
+- The file exists under the same path in both branches
+
+If any file with conflicts in that merge request does not meet all of these
+criteria, the conflicts for that merge request cannot be resolved in the UI.
+
+Additionally, GitLab does not detect conflicts in renames away from a path. For
+example, this will not create a conflict: on branch `a`, doing `git mv file1
+file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
+present in the branch after the merge request is merged.
+
+[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
new file mode 100644
index 0000000000000000000000000000000000000000..5ead9f4177f2bb35a85a1a971330b1be64d61772
--- /dev/null
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -0,0 +1,64 @@
+# Reverting changes
+
+> [Introduced][ce-1990] in GitLab 8.5.
+
+---
+
+GitLab implements Git's powerful feature to [revert any commit][git-revert]
+with introducing a **Revert** button in Merge Requests and commit details.
+
+## Reverting a Merge Request
+
+_**Note:** The **Revert** button will only be available for Merge Requests
+created since GitLab 8.5. However, you can still revert a Merge Request
+by reverting the merge commit from the list of Commits page._
+
+After the Merge Request has been merged, a **Revert** button will be available
+to revert the changes introduced by that Merge Request:
+
+![Revert Merge Request](img/revert_changes_mr.png)
+
+---
+
+You can revert the changes directly into the selected branch or you can opt to
+create a new Merge Request with the revert changes:
+
+![Revert Merge Request modal](img/revert_changes_mr_modal.png)
+
+---
+
+After the Merge Request has been reverted, the **Revert** button will not be
+available anymore.
+
+## Reverting a Commit
+
+You can revert a Commit from the Commit details page:
+
+![Revert commit](img/revert_changes_commit.png)
+
+---
+
+Similar to reverting a Merge Request, you can opt to revert the changes
+directly into the target branch or create a new Merge Request to revert the
+changes:
+
+![Revert commit modal](img/revert_changes_commit_modal.png)
+
+---
+
+After the Commit has been reverted, the **Revert** button will not be available
+anymore.
+
+Please note that when reverting merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to revert a merge commit using the second parent as the
+mainline:
+
+```bash
+git revert -m 2 7a39eb0
+```
+
+[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
+[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
new file mode 100644
index 0000000000000000000000000000000000000000..77eab7ba5e3ce043f92dea39e26dd20a0790b707
--- /dev/null
+++ b/doc/user/project/merge_requests/versions.md
@@ -0,0 +1,42 @@
+# Merge requests versions
+
+> Will be [introduced][ce-5467] in GitLab 8.12.
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+![Merge request versions](img/versions.png)
+
+---
+
+By default, the latest version of changes is shown. However, you
+can select an older one from version dropdown.
+
+![Merge request versions dropdown](img/versions_dropdown.png)
+
+---
+
+You can also compare the merge request version with an older one to see what has
+changed since then.
+
+![Merge request versions compare](img/versions_compare.png)
+
+---
+
+Every time you push new changes to the branch, a link to compare the last
+changes appears as a system note.
+
+![Merge request versions system note](img/versions_system_note.png)
+
+---
+
+>**Notes:**
+- Comments are disabled while viewing outdated merge versions or comparing to
+  versions other than base.
+- Merge request versions are based on push not on commit. So, if you pushed 5
+  commits in a single push, it will be a single option in the dropdown. If you
+  pushed 5 times, that will count for 5 options.
+
+[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
new file mode 100644
index 0000000000000000000000000000000000000000..546c8bdc5e55e970e1066afdf9c9f1433ff3c28e
--- /dev/null
+++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
@@ -0,0 +1,17 @@
+# "Work In Progress" Merge Requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked a **Work In Progress**.
+
+![Blocked Accept Button](img/wip_blocked_accept_button.png)
+
+To mark a merge request a Work In Progress, simply start its title with `[WIP]`
+or `WIP:`.
+
+![Mark as WIP](img/wip_mark_as_wip.png)
+
+To allow a Work In Progress merge request to be accepted again when it's ready,
+simply remove the `WIP` prefix.
+
+![Unark as WIP](img/wip_unmark_as_wip.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
new file mode 100644
index 0000000000000000000000000000000000000000..60b7bec2ba7277c7111812a5c174dc8720037f71
--- /dev/null
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -0,0 +1,317 @@
+# New CI build permissions model
+
+> Introduced in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned [build permissions] system. You can find
+all discussion and all our concerns when choosing the current approach in issue
+[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994).
+
+---
+
+Builds permissions should be tightly integrated with the permissions of a user
+who is triggering a build.
+
+The reasons to do it like that are:
+
+- We already have a permissions system in place: group and project membership
+  of users.
+- We already fully know who is triggering a build (using `git push`, using the
+  web UI, executing triggers).
+- We already know what user is allowed to do.
+- We use the user permissions for builds that are triggered by the user.
+- It opens a lot of possibilities to further enforce user permissions, like
+  allowing only specific users to access runners or use secure variables and
+  environments.
+- It is simple and convenient that your build can access everything that you
+  as a user have access to.
+- Short living unique tokens are now used, granting access for time of the build
+  and maximizing security.
+
+With the new behavior, any build that is triggered by the user, is also marked
+with their permissions. When a user does a `git push` or changes files through
+the web UI, a new pipeline will be usually created. This pipeline will be marked
+as created be the pusher (local push or via the UI) and any build created in this
+pipeline will have the permissions of the pusher.
+
+This allows us to make it really easy to evaluate the access for all projects
+that have Git submodules or are using container images that the pusher would
+have access too. **The permission is granted only for time that build is running.
+The access is revoked after the build is finished.**
+
+## Types of users
+
+It is important to note that we have a few types of users:
+
+- **Administrators**: CI builds created by Administrators will not have access
+  to all GitLab projects, but only to projects and container images of projects
+  that the administrator is a member of.That means that if a project is either
+  public or internal users have access anyway, but if a project is private, the
+  Administrator will have to be a member of it in order to have access to it
+  via another project's build.
+
+- **External users**: CI builds created by [external users][ext] will have
+  access only to projects to which user has at least reporter access. This
+  rules out accessing all internal projects by default,
+
+This allows us to make the CI and permission system more trustworthy.
+Let's consider the following scenario:
+
+1. You are an employee of a company. Your company has a number of internal tools
+   hosted in private repositories and you have multiple CI builds that make use
+   of these repositories.
+
+2. You invite a new [external user][ext]. CI builds created by that user do not
+   have access to internal repositories, because the user also doesn't have the
+   access from within GitLab. You as an employee have to grant explicit access
+   for this user. This allows us to prevent from accidental data leakage.
+
+## Build token
+
+A unique build token is generated for each build and it allows the user to
+access all projects that would be normally accessible to the user creating that
+build.
+
+We try to make sure that this token doesn't leak by:
+
+1. Securing all API endpoints to not expose the build token.
+1. Masking the build token from build logs.
+1. Allowing to use the build token **only** when build is running.
+
+However, this brings a question about the Runners security. To make sure that
+this token doesn't leak, you should also make sure that you configure
+your Runners in the most possible secure way, by avoiding the following:
+
+1. Any usage of Docker's `privileged` mode is risky if the machines are re-used.
+1. Using the `shell` executor since builds run on the same machine.
+
+By using an insecure GitLab Runner configuration, you allow the rogue developers
+to steal the tokens of other builds.
+
+## Build triggers
+
+[Build triggers][triggers] do not support the new permission model.
+They continue to use the old authentication mechanism where the CI build
+can access only its own sources. We plan to remove that limitation in one of
+the upcoming releases.
+
+## Before GitLab 8.12
+
+In versions before GitLab 8.12, all CI builds would use the CI Runner's token
+to checkout project sources.
+
+The project's Runner's token was a token that you could find under the
+project's **Settings > CI/CD Pipelines** and was limited to access only that
+project.
+It could be used for registering new specific Runners assigned to the project
+and to checkout project sources.
+It could also be used with the GitLab Container Registry for that project,
+allowing pulling and pushing Docker images from within the CI build.
+
+---
+
+GitLab would create a special checkout URL like:
+
+```
+https://gitlab-ci-token:<project-runners-token>/gitlab.com/gitlab-org/gitlab-ce.git
+```
+
+And then the users could also use it in their CI builds 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
+```
+
+Using single token had multiple security implications:
+
+- The token would be readable to anyone who had developer access to a project
+  that could run CI builds, allowing the developer to register any specific
+  Runner for that project.
+- The token would allow to access only the project's sources, forbidding from
+  accessing any other projects.
+- The token was not expiring and was multi-purpose: used for checking out sources,
+  for registering specific runners and for accessing a project's container
+  registry with read-write permissions.
+
+All the above led to a new permission model for builds that was introduced
+with GitLab 8.12.
+
+## Making use of the new CI build permissions model
+
+With the new build permissions model, there is now an easy way to access all
+dependent source code in a project. That way, we can:
+
+1. Access a project's Git submodules
+1. Access private container images
+1. Access project's and submodule LFS objects
+
+Below you can see the prerequisites needed to make use of the new permissions
+model and how that works with Git submodules and private Docker images hosted on
+the container registry.
+
+### Prerequisites to use the new permissions model
+
+With the new permissions model in place, there may be times that your build will
+fail. This is most likely because your project tries to access other project's
+sources, and you don't have the appropriate permissions. In the build log look
+for information about 403 or forbidden access messages.
+
+In short here's what you need to do should you encounter any issues.
+
+As an administrator:
+
+- **500 errors**: You will need to update [GitLab Workhorse][workhorse] to at
+  least 0.8.2. This is done automatically for Omnibus installations, you need to
+  [check manually][update-docs] for installations from source.
+- **500 errors**: Check if you have another web proxy sitting in front of NGINX (HAProxy,
+  Apache, etc.). It might be a good idea to let GitLab use the internal NGINX
+  web server and not disable it completely. See [this comment][comment] for an
+  example.
+- **403 errors**: You need to make sure that your installation has [HTTP(S)
+  cloning enabled][https]. HTTP(S) support is now a **requirement** by GitLab CI
+  to clone all sources.
+
+As a user:
+
+- Make sure you are a member of the group or project you're trying to have
+  access to. As an Administrator, you can verify that by impersonating the user
+  and retry the failing build in order to verify that everything is correct.
+
+### Git submodules
+
+>
+It often happens that while working on one project, you need to use another
+project from within it; perhaps it’s a library that a third party developed or
+you’re developing a project separately and are using it in multiple parent
+projects.
+A common issue arises in these scenarios: you want to be able to treat the two
+projects as separate yet still be able to use one from within the other.
+>
+_Excerpt from the [Git website][git-scm] about submodules._
+
+If dealing with submodules, your project will probably have a file named
+`.gitmodules`. And this is how it usually looks like:
+
+```
+[submodule "tools"]
+	path = tools
+	url = git@gitlab.com/group/tools.git
+```
+
+> **Note:**
+If you are **not** using GitLab 8.12 or higher, you would need to work your way
+around this issue in order to access the sources of `gitlab.com/group/tools`
+(e.g., use [SSH keys](../ssh_keys/README.md)).
+>
+With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+can access. More information about how this system works can be found in the
+[Build permissions model](../../user/permissions.md#builds-permissions).
+
+To make use of the new changes, you have to update your `.gitmodules` file to
+use a relative URL.
+
+Let's consider the following example:
+
+1. Your project is located at `https://gitlab.com/secret-group/my-project`.
+1. To checkout your sources you usually use an SSH address like
+   `git@gitlab.com:secret-group/my-project.git`.
+1. Your project depends on `https://gitlab.com/group/tools`.
+1. You have the `.gitmodules` file with above content.
+
+Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
+this easily allows you to use HTTP for cloning all your CI builds and SSH
+for all your local checkouts.
+
+For example, if you change the `url` of your `tools` dependency, from
+`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct
+Git to automatically deduce the URL that should be used when cloning sources.
+Whether you use HTTP or SSH, Git will use that same channel and it will allow
+to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning
+your sources), and all your local clones will continue using SSH.
+
+Given the above explanation, your `.gitmodules` file should eventually look
+like this:
+
+```
+[submodule "tools"]
+	path = tools
+	url = ../../group/tools.git
+```
+
+However, you have to explicitly tell GitLab CI to clone your submodules as this
+is not done automatically. You can achieve that by adding a `before_script`
+section to your `.gitlab-ci.yml`:
+
+```
+before_script:
+  - git submodule update --init --recursive
+
+test:
+  script:
+    - run-my-tests
+```
+
+This will make GitLab CI initialize (fetch) and update (checkout) all your
+submodules recursively.
+
+If Git does not use the newly added relative URLs but still uses your old URLs,
+you might need to add `git submodule sync --recursive` to your `.gitlab-ci.yml`,
+prior to running `git submodule update --init --recursive`. This transfers the
+changes from your `.gitmodules` file into the `.git` folder, which is kept by
+runners between runs.
+
+In case your environment or your Docker image doesn't have Git installed,
+you have to either ask your Administrator or install the missing dependency
+yourself:
+
+```
+# Debian / Ubuntu
+before_script:
+  - apt-get update -y
+  - apt-get install -y git-core
+  - git submodule update --init --recursive
+
+# CentOS / RedHat
+before_script:
+  - yum install git
+  - git submodule update --init --recursive
+
+# Alpine
+before_script:
+  - apk add -U git
+  - git submodule update --init --recursive
+```
+
+### Container Registry
+
+With the update permission model we also extended the support for accessing
+Container Registries for private projects.
+
+> **Note:**
+As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
+permissions, this makes the `image:` directive to not work with private projects
+automatically. The manual configuration by an Administrator is required to use
+private images. We plan to remove that limitation in one of the upcoming releases.
+
+Your builds can access all container images that you would normally have access
+to. The only implication is that you can push to the Container Registry of the
+project for which the build is triggered.
+
+This is how an example usage can look like:
+
+```
+test:
+  script:
+    - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+    - docker pull $CI_REGISTRY/group/other-project:latest
+    - docker run $CI_REGISTRY/group/other-project:latest
+```
+
+[build permissions]: ../permissions.md#builds-permissions
+[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
+[ext]: ../permissions.md#external-users
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
+[triggers]: ../../ci/triggers/README.md
+[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
+[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
diff --git a/doc/user/project/pipelines/img/pipelines_settings_badges.png b/doc/user/project/pipelines/img/pipelines_settings_badges.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0c4640791d353c985fc23e036f3618e95c260ac
Binary files /dev/null and b/doc/user/project/pipelines/img/pipelines_settings_badges.png differ
diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2a5568521ff4f3184f686adad56b3ca3c3eca3a
Binary files /dev/null and b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
new file mode 100644
index 0000000000000000000000000000000000000000..3823100daf25f15f7c545f3e94b7fbefc213831f
Binary files /dev/null and b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4f78803e6979122cfa11e903ad51374dbceab7e
Binary files /dev/null and b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png differ
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
new file mode 100644
index 0000000000000000000000000000000000000000..6cbcf3c400f66bc17fc9141ba29b524f77a969ab
--- /dev/null
+++ b/doc/user/project/pipelines/settings.md
@@ -0,0 +1,113 @@
+# CI/CD pipelines settings
+
+To reach the pipelines settings:
+
+1. Navigate to your project and click the cog icon in the upper right corner.
+
+    ![Project settings menu](../img/project_settings_list.png)
+
+1. Select **CI/CD Pipelines** from the menu.
+
+The following settings can be configured per project.
+
+## Git strategy
+
+With Git strategy, you can choose the default way your repository is fetched
+from GitLab in a job.
+
+There are two options:
+
+- Using `git clone` which is slower since it clones the repository from scratch
+  for every job, ensuring that the project workspace is always pristine.
+- Using `git fetch` which is faster as it re-uses the project workspace (falling
+  back to clone if it doesn't exist).
+
+The default Git strategy can be overridden by the [GIT_STRATEGY variable][var]
+in `.gitlab-ci.yml`.
+
+## Timeout
+
+Timeout defines the maximum amount of time in minutes that a job is able run.
+The default value is 60 minutes. Decrease the time limit if you want to impose
+a hard limit on your jobs' running time or increase it otherwise. In any case,
+if the job surpasses the threshold, it is marked as failed.
+
+## Test coverage parsing
+
+If you use test coverage in your code, GitLab can capture its output in the
+build log using a regular expression. In the pipelines settings, search for the
+"Test coverage parsing" section.
+
+![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png)
+
+Leave blank if you want to disable it or enter a ruby regular expression. You
+can use http://rubular.com to test your regex.
+
+If the pipeline succeeds, the coverage is shown in the merge request widget and
+in the builds table.
+
+![MR widget coverage](img/pipelines_test_coverage_mr_widget.png)
+
+![Build status coverage](img/pipelines_test_coverage_build.png)
+
+A few examples of known coverage tools for a variety of languages can be found
+in the pipelines settings page.
+
+## Visibility of pipelines
+
+For public and internal projects, the pipelines page can be accessed by
+anyone and those logged in respectively. If you wish to hide it so that only
+the members of the project or group have access to it, uncheck the **Public
+pipelines** checkbox and save the changes.
+
+## Badges
+
+In the pipelines settings page you can find build status and test coverage
+badges for your project. The latest successful pipeline will be used to read
+the build status and test coverage values.
+
+Visit the pipelines settings page in your project to see the exact link to
+your badges, as well as ways to embed the badge image in your HTML or Markdown
+pages.
+
+![Pipelines badges](img/pipelines_settings_badges.png)
+
+### Build status badge
+
+Depending on the status of your build, a badge can have the following values:
+
+- running
+- success
+- failed
+- skipped
+- unknown
+
+You can access a build status badge image using the following link:
+
+```
+https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg
+```
+
+### Test coverage report badge
+
+GitLab makes it possible to define the regular expression for [coverage report],
+that each build log will be matched against. This means that each build in the
+pipeline can have the test coverage percentage value defined.
+
+The test coverage badge can be accessed using following link:
+
+```
+https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg
+```
+
+If you would like to get the coverage report from a specific job, you can add
+the `job=coverage_job_name` parameter to the URL. For example, the following
+Markdown code will embed the test coverage report badge of the `coverage` job
+into your `README.md`:
+
+```markdown
+![coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
+```
+
+[var]: ../../../ci/yaml/README.md#git-strategy
+[coverage report]: #test-coverage-parsing
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 96d9bdc1b29354b5109511a555b5af5f656cc1c6..f7a686d2ccf4dd7eb3c4b3bde50d31976709b825 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -5,6 +5,8 @@ idea of having read or write permission to the repository and branches. To
 prevent people from messing with history or pushing code without review, we've
 created protected branches.
 
+## Overview
+
 By default, a protected branch does four simple things:
 
 - it prevents its creation, if not already created, from everybody except users
@@ -15,6 +17,11 @@ By default, a protected branch does four simple things:
 
 See the [Changelog](#changelog) section for changes over time.
 
+>
+>Additional functionality for GitLab Enterprise Edition:
+>
+>- Restrict push and merge access to [certain users][ee-restrict]
+
 ## Configuring protected branches
 
 To protect a branch, you need to have at least Master permission level. Note
@@ -28,22 +35,41 @@ that the `master` branch is protected by default.
 1. From the **Branch** dropdown menu, select the branch you want to protect and
    click **Protect**. In the screenshot below, we chose the `develop` branch.
 
-    ![Choose protected branch](img/protected_branches_choose_branch.png)
+    ![Protected branches page](img/protected_branches_page.png)
 
-1. Once done, the protected branch will appear in the "Already protected" list.
+1. Once done, the protected branch will appear in the "Protected branches" list.
 
     ![Protected branches list](img/protected_branches_list.png)
 
+## Using the Allowed to merge and Allowed to push settings
+
+> [Introduced][ce-5081] in GitLab 8.11.
+
+Since GitLab 8.11, we added another layer of branch protection which provides
+more granular management of protected branches. The "Developers can push"
+option was replaced by an "Allowed to push" setting which can be set to
+allow/prohibit Masters and/or Developers to push to a protected branch.
+
+Using the "Allowed to push" and "Allowed to merge" settings, you can control
+the actions that different roles can perform with the protected branch.
+For example, you could set "Allowed to push" to "No one", and "Allowed to merge"
+to "Developers + Masters", to require _everyone_ to submit a merge request for
+changes going into the protected branch. This is compatible with workflows like
+the [GitLab workflow](../../workflow/gitlab_flow.md).
+
+However, there are workflows where that is not needed, and only protecting from
+force pushes and branch removal is useful. For those workflows, you can allow
+everyone with write access to push to a protected branch by setting
+"Allowed to push" to "Developers + Masters".
+
+You can set the "Allowed to push" and "Allowed to merge" options while creating
+a protected branch or afterwards by selecting the option you want from the
+dropdown list in the "Already protected" area.
 
-Since GitLab 8.10, we added another layer of branch protection which provides
-more granular management of protected branches. You can now choose the option
-"Developers can merge" so that Developer users can merge a merge request but
-not directly push. In that case, your branches are protected from direct pushes,
-yet Developers don't need elevated permissions or wait for someone with a higher
-permission level to press merge.
+![Developers can push](img/protected_branches_devs_can_push.png)
 
-You can set this option while creating the protected branch or after its
-creation.
+If you don't choose any of those options while creating a protected branch,
+they are set to "Masters" by default.
 
 ## Wildcard protected branches
 
@@ -66,40 +92,25 @@ Two different wildcards can potentially match the same branch. For example,
 In that case, if _any_ of these protected branches have a setting like
 "Allowed to push", then `production-stable` will also inherit this setting.
 
-If you click on a protected branch's name that is created using a wildcard,
-you will be presented with a list of all matching branches:
+If you click on a protected branch's name, you will be presented with a list of
+all matching branches:
 
 ![Protected branch matches](img/protected_branches_matches.png)
 
-## Restrict the creation of protected branches
-
-Creating a protected branch or a list of protected branches using the wildcard
-feature, not only you are restricting pushes to those branches, but also their
-creation if not already created.
-
-## Error messages when pushing to a protected branch
-
-A user with insufficient permissions will be presented with an error when
-creating or pushing to a branch that's prohibited, either through GitLab's UI:
-
-![Protected branch error GitLab UI](img/protected_branches_error_ui.png)
-
-or using Git from their terminal:
+## Changelog
 
-```bash
-remote: GitLab: You are not allowed to push code to protected branches on this project.
-To https://gitlab.example.com/thedude/bowling.git
- ! [remote rejected] staging-stable -> staging-stable (pre-receive hook declined)
-error: failed to push some refs to 'https://gitlab.example.com/thedude/bowling.git'
-```
+**8.11**
 
-## Changelog
+- Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081]
 
-**8.10.0**
+**8.10**
 
-- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!5081][ce-4665]
+- Allow developers to merge into a protected branch without having push access [gitlab-org/gitlab-ce!4892][ce-4892]
+- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!4665][ce-4665]
 
 ---
 
 [ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards"
+[ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access"
 [ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to"
+[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users
diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_branch_dropdown.png
rename to doc/user/project/repository/img/web_editor_new_branch_dropdown.png
diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0a63ddf0ab697740f34613a819491724468cff3
Binary files /dev/null and b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png differ
diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_branch_page.png
rename to doc/user/project/repository/img/web_editor_new_branch_page.png
diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_directory_dialog.png
rename to doc/user/project/repository/img/web_editor_new_directory_dialog.png
diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_directory_dropdown.png
rename to doc/user/project/repository/img/web_editor_new_directory_dropdown.png
diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_file_dropdown.png
rename to doc/user/project/repository/img/web_editor_new_file_dropdown.png
diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_file_editor.png
rename to doc/user/project/repository/img/web_editor_new_file_editor.png
diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_push_widget.png
rename to doc/user/project/repository/img/web_editor_new_push_widget.png
diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_tag_dropdown.png
rename to doc/user/project/repository/img/web_editor_new_tag_dropdown.png
diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png
similarity index 100%
rename from doc/workflow/img/web_editor_new_tag_page.png
rename to doc/user/project/repository/img/web_editor_new_tag_page.png
diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
similarity index 100%
rename from doc/workflow/img/web_editor_start_new_merge_request.png
rename to doc/user/project/repository/img/web_editor_start_new_merge_request.png
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
new file mode 100644
index 0000000000000000000000000000000000000000..4efc51cc42366b4846a4f4e0abdd9a5264752725
Binary files /dev/null and b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
new file mode 100644
index 0000000000000000000000000000000000000000..67190c58823e5e7c5cbb867ccf27638f925b9b50
Binary files /dev/null and b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
new file mode 100644
index 0000000000000000000000000000000000000000..47719113805ff8811a879a6c4ff0360a50cf7b30
Binary files /dev/null and b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png differ
diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
similarity index 100%
rename from doc/workflow/img/web_editor_upload_file_dialog.png
rename to doc/user/project/repository/img/web_editor_upload_file_dialog.png
diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
similarity index 100%
rename from doc/workflow/img/web_editor_upload_file_dropdown.png
rename to doc/user/project/repository/img/web_editor_upload_file_dropdown.png
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
new file mode 100644
index 0000000000000000000000000000000000000000..675e89e42475268282d53fbf3c4682b35ec33408
--- /dev/null
+++ b/doc/user/project/repository/web_editor.md
@@ -0,0 +1,175 @@
+# GitLab Web Editor
+
+Sometimes it's easier to make quick changes directly from the GitLab interface
+than to clone the project and use the Git command line tool. In this feature
+highlight we look at how you can create a new file, directory, branch or
+tag from the file browser. All of these actions are available from a single
+dropdown menu.
+
+## Create a file
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New file** from the dropdown.
+
+![New file dropdown menu](img/web_editor_new_file_dropdown.png)
+
+---
+
+Enter a file name in the **File name** box. Then, add file content in the editor
+area. Add a descriptive commit message and choose a branch. The branch field
+will default to the branch you were viewing in the file browser. If you enter
+a new branch name, a checkbox will appear allowing you to start a new merge
+request after you commit the changes.
+
+When you are satisfied with your new file, click **Commit Changes** at the bottom.
+
+![Create file editor](img/web_editor_new_file_editor.png)
+
+### Template dropdowns
+
+When starting a new project, there are some common files which the new project
+might need too. Therefore a message will be displayed by GitLab to make this
+easy for you.
+
+![First file for your project](img/web_editor_template_dropdown_first_file.png)
+
+When clicking on either `LICENSE` or `.gitignore`, a dropdown will be displayed
+to provide you with a template which might be suitable for your project.
+
+![MIT license selected](img/web_editor_template_dropdown_mit_license.png)
+
+The license, changelog, contribution guide, or `.gitlab-ci.yml` file could also
+be added through a button on the project page. In the example below the license
+has already been created, which creates a link to the license itself.
+
+![New file button](img/web_editor_template_dropdown_buttons.png)
+
+>**Note:**
+The **Set up CI** button will not appear on an empty repository. You have to at
+least add a file in order for the button to show up.
+
+## Upload a file
+
+The ability to create a file is great when the content is text. However, this
+doesn't work well for binary data such as images, PDFs or other file types. In
+this case you need to upload a file.
+
+From a project's files page, click the '+' button to the right of the branch
+selector. Choose **Upload file** from the dropdown.
+
+![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
+
+---
+
+Once the upload dialog pops up there are two ways to upload your file. Either
+drag and drop a file on the pop up or use the **click to upload** link. A file
+preview will appear once you have selected a file to upload.
+
+Enter a commit message, choose a branch, and click **Upload file** when you are
+ready.
+
+![Upload file dialog](img/web_editor_upload_file_dialog.png)
+
+## Create a directory
+
+To keep files in the repository organized it is often helpful to create a new
+directory.
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New directory** from the dropdown.
+
+![New directory dropdown](img/web_editor_new_directory_dropdown.png)
+
+---
+
+In the new directory dialog enter a directory name, a commit message and choose
+the target branch. Click **Create directory** to finish.
+
+![New directory dialog](img/web_editor_new_directory_dialog.png)
+
+## Create a new branch
+
+There are multiple ways to create a branch from GitLab's web interface.
+
+### Create a new branch from an issue
+
+> [Introduced][ce-2808] in GitLab 8.6.
+
+In case your development workflow dictates to have an issue for every merge
+request, you can quickly create a branch right on the issue page which will be
+tied with the issue itself. You can see a **New branch** button after the issue
+description, unless there is already a branch with the same name or a referenced
+merge request.
+
+![New Branch Button](img/web_editor_new_branch_from_issue.png)
+
+Once you click it, a new branch will be created that diverges from the default
+branch of your project, by default `master`. The branch name will be based on
+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`.
+
+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.
+
+### Create a new branch from a project's dashboard
+
+If you want to make changes to several files before creating a new merge
+request, you can create a new branch up front. From a project's files page,
+choose **New branch** from the dropdown.
+
+![New branch dropdown](img/web_editor_new_branch_dropdown.png)
+
+---
+
+Enter a new **Branch name**. Optionally, change the **Create from** field
+to choose which branch, tag or commit SHA this new branch will originate from.
+This field will autocomplete if you start typing an existing branch or tag.
+Click **Create branch** and you will be returned to the file browser on this new
+branch.
+
+![New branch page](img/web_editor_new_branch_page.png)
+
+---
+
+You can now make changes to any files, as needed. When you're ready to merge
+the changes back to master you can use the widget at the top of the screen.
+This widget only appears for a period of time after you create the branch or
+modify files.
+
+![New push widget](img/web_editor_new_push_widget.png)
+
+## Create a new tag
+
+Tags are useful for marking major milestones such as production releases,
+release candidates, and more. You can create a tag from a branch or a commit
+SHA. From a project's files page, choose **New tag** from the dropdown.
+
+![New tag dropdown](img/web_editor_new_tag_dropdown.png)
+
+---
+
+Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
+would like to create this new tag. You can optionally add a message and
+release notes. The release notes section supports markdown format and you can
+also upload an attachment. Click **Create tag** and you will be taken to the tag
+list page.
+
+![New tag page](img/web_editor_new_tag_page.png)
+
+## Tips
+
+When creating or uploading a new file, or creating a new directory, you can
+trigger a new merge request rather than committing directly to master. Enter
+a new branch name in the **Target branch** field. You will notice a checkbox
+appear that is labeled **Start a new merge request with these changes**. After
+you commit the changes you will be taken to a new merge request form.
+
+![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
+
+![New file button](basicsimages/file_button.png)
+[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
+[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 08ff89ce6aeee7c2b128074278b8c50728c92cd9..dfc762fe1d3501874f48084ab1821093ac55aa23 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -3,11 +3,12 @@
 >**Notes:**
 >
 >  - [Introduced][ce-3050] in GitLab 8.9.
->  - Importing will not be possible if the import instance version is lower
->    than that of the exporter.
+>  - Importing will not be possible if the import instance version differs from
+>    that of the exporter.
 >  - For existing installations, the project import option has to be enabled in
 >    application settings (`/admin/application_settings`) under 'Import sources'.
->    You will have to be an administrator to enable and use the import functionality.
+>    Ask your administrator if you don't see the **GitLab export** button when
+>    creating a new project.
 >  - You can find some useful raketasks if you are an administrator in the
 >    [import_export](../../../administration/raketasks/project_import_export.md)
 >    raketask.
@@ -17,6 +18,21 @@
 Existing projects running on any GitLab instance or GitLab.com can be exported
 with all their related data and be moved into a new GitLab instance.
 
+## Version history
+
+| GitLab version | Import/Export version |
+| -------- | -------- |
+| 8.13.0 to current  | 0.1.5    |
+| 8.12.0   | 0.1.4    |
+| 8.10.3   | 0.1.3    |
+| 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.
+
 ## Exported contents
 
 The following items will be exported:
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
new file mode 100644
index 0000000000000000000000000000000000000000..5f6a6c6503e4e72adab1bcf163f987892476db32
--- /dev/null
+++ b/doc/user/project/slash_commands.md
@@ -0,0 +1,31 @@
+# GitLab slash commands
+
+Slash commands are textual shortcuts for common actions on issues or merge
+requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
+You can enter these commands while creating a new issue or merge request, and
+in comments. Each command should be on a separate line in order to be properly
+detected and executed. The commands are removed from the issue, merge request or
+comment body before it is saved and will not be visible to anyone else.
+
+Below is a list of all of the available commands and descriptions about what they
+do.
+
+| Command                    | Action       |
+|:---------------------------|:-------------|
+| `/close`                   | Close the issue or merge request |
+| `/reopen`                  | Reopen the issue or merge request |
+| `/title <New title>`       | Change title |
+| `/assign @username`        | Assign |
+| `/unassign`                | Remove assignee |
+| `/milestone %milestone`    | Set milestone |
+| `/remove_milestone`        | Remove milestone |
+| `/label ~foo ~"bar baz"`   | Add label(s) |
+| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) |
+| `/relabel ~foo ~"bar baz"` | Replace all label(s) |
+| `/todo`                    | Add a todo |
+| `/done`                    | Mark todo as done |
+| `/subscribe`               | Subscribe |
+| `/unsubscribe`             | Unsubscribe |
+| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
+| `/remove_due_date`         | Remove due date |
+| `/wip`                     | Toggle the Work In Progress status |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 993349e5b46f2d21afb794ecb8267e03669adacf..2d9bfbc062902a88ee64e5d7e655d4cd0ab5be77 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,10 +1,13 @@
 # Workflow
 
-- [Authorization for merge requests](authorization_for_merge_requests.md)
+- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
 - [Change your time zone](timezone.md)
+- [Cycle Analytics](../user/project/cycle_analytics.md)
+- [Description templates](../user/project/description_templates.md)
 - [Feature branch workflow](workflow.md)
 - [GitLab Flow](gitlab_flow.md)
 - [Groups](groups.md)
+- [Issue Board](../user/project/issue_board.md)
 - [Keyboard shortcuts](shortcuts.md)
 - [File finder](file_finder.md)
 - [Labels](../user/project/labels.md)
@@ -13,17 +16,21 @@
 - [Project forking workflow](forking_workflow.md)
 - [Project users](add-user/add-user.md)
 - [Protected branches](../user/project/protected_branches.md)
+- [Slash commands](../user/project/slash_commands.md)
 - [Sharing a project with a group](share_with_group.md)
 - [Share projects with other groups](share_projects_with_other_groups.md)
-- [Web Editor](web_editor.md)
+- [Web Editor](../user/project/repository/web_editor.md)
 - [Releases](releases.md)
-- [Issuable Templates](issuable_templates.md)
 - [Milestones](milestones.md)
-- [Merge Requests](merge_requests.md)
-- [Revert changes](revert_changes.md)
-- [Cherry-pick changes](cherry_pick_changes.md)
-- ["Work In Progress" Merge Requests](wip_merge_requests.md)
-- [Merge When Build Succeeds](merge_when_build_succeeds.md)
+- [Merge Requests](../user/project/merge_requests.md)
+  - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
+  - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
+  - [Merge when build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md)
+  - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md)
+  - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
+  - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
+  - [Merge requests versions](../user/project/merge_requests/versions.md)
+  - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
 - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
 - [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
 - [Todos](todos.md)
diff --git a/doc/workflow/authorization_for_merge_requests.md b/doc/workflow/authorization_for_merge_requests.md
index d1d6d94ec11833256a6fb8af214b63d99cf9ab9d..7bf80a3ad0db4dd08b8f9ab35cce31b00b7193e1 100644
--- a/doc/workflow/authorization_for_merge_requests.md
+++ b/doc/workflow/authorization_for_merge_requests.md
@@ -1,40 +1 @@
-# Authorization for Merge requests
-
-There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project.
-
-## Protected branch flow
-
-With the protected branch flow everybody works within the same GitLab project.
-
-The project maintainers get Master access and the regular developers get Developer access.
-
-The maintainers mark the authoritative branches as 'Protected'.
-
-The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches.
-
-Only users with Master access can merge changes into a protected branch.
-
-### Advantages
-
-- fewer projects means less clutter
-- developers need to consider only one remote repository
-
-### Disadvantages
-
-- manual setup of protected branch required for each new project
-
-## Forking workflow
-
-With the forking workflow the maintainers get Master access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it.
-
-Developers create forks of the authoritative project and push their feature branches to their own forks.
-
-To get their changes into master they need to create a merge request across forks.
-
-### Advantages
-
-- in an appropriately configured GitLab group, new projects automatically get the required access restrictions for regular developers: fewer manual steps to configure authorization for new projects
-
-### Disadvantages
-
-- the project need to keep their forks up to date, which requires more advanced Git skills (managing multiple remotes)
+This document was moved to [user/project/merge_requests/authorization_for_merge_requests](../user/project/merge_requests/authorization_for_merge_requests.md)
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
index 64b94d810242317c9f6c70827ed527ab4ca39767..663ffd3f746f2c7ee288ca65291cac6464a8aefe 100644
--- a/doc/workflow/cherry_pick_changes.md
+++ b/doc/workflow/cherry_pick_changes.md
@@ -1,52 +1 @@
-# Cherry-pick changes
-
-> [Introduced][ce-3514] in GitLab 8.7.
-
----
-
-GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
-with introducing a **Cherry-pick** button in Merge Requests and commit details.
-
-## Cherry-picking a Merge Request
-
-After the Merge Request has been merged, a **Cherry-pick** button will be available
-to cherry-pick the changes introduced by that Merge Request:
-
-![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
-
----
-
-You can cherry-pick the changes directly into the selected branch or you can opt to
-create a new Merge Request with the cherry-pick changes:
-
-![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
-
-## Cherry-picking a Commit
-
-You can cherry-pick a Commit from the Commit details page:
-
-![Cherry-pick commit](img/cherry_pick_changes_commit.png)
-
----
-
-Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
-directly into the target branch or create a new Merge Request to cherry-pick the
-changes:
-
-![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
-
----
-
-Please note that when cherry-picking merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to cherry-pick a merge commit using the second parent as the
-mainline:
-
-```bash
-git cherry-pick -m 2 7a39eb0
-```
-
-[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
-[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
+This document was moved to [user/project/merge_requests/cherry_pick_changes](../user/project/merge_requests/cherry_pick_changes.md).
diff --git a/doc/workflow/description_templates.md b/doc/workflow/description_templates.md
deleted file mode 100644
index 9514564af023daa4264a8eb9ce6166617b678671..0000000000000000000000000000000000000000
--- a/doc/workflow/description_templates.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Description templates
-
-Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
-
-Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
-
-Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
-
-![Description templates](img/description_templates.png)
-
-_Example:_
-`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2b2f140f8bf195ad357b607d0337771023b549fd..2215f37b81aab3c6daf3898291dd469736a14ecb 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -89,7 +89,7 @@ In this case the master branch is deployed on staging. When someone wants to dep
 And going live with code happens by merging the pre-production branch into the production branch.
 This workflow where commits only flow downstream ensures that everything has been tested on all environments.
 If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
-If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
+If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
 If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
 An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
 
@@ -115,7 +115,7 @@ In this flow it is not common to have a production branch (or git flow master br
 
 Merge or pull requests are created in a git management application and ask an assigned person to merge two branches.
 Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch.
-Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.
+Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee.
 In this article we'll refer to them as merge requests.
 
 If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team.
@@ -228,7 +228,7 @@ We'll discuss the three reasons to merge in master: leveraging code, merge confl
 If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit.
 If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this.
 You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order.
-For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it.
+For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG.md merge=union` so that there are fewer merge conflicts in it.
 The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project.
 Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI).
 At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
diff --git a/doc/workflow/img/description_templates.png b/doc/workflow/img/description_templates.png
deleted file mode 100644
index af2e9403826121a061c882ff509c8ba5653673b7..0000000000000000000000000000000000000000
Binary files a/doc/workflow/img/description_templates.png and /dev/null differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png
index b6ed8dd692aa5140cdae9839c55869139573efd3..eadd33c695f2bfa90150a8bd3bafc7efd2fb7378 100644
Binary files a/doc/workflow/importing/img/import_projects_from_github_importer.png and b/doc/workflow/importing/img/import_projects_from_github_importer.png differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
index c8f35a50f48dcfdc60c1cfd0fec986936b821894..6e91c430a33c22c288e0b57608ed5b0968fb885c 100644
Binary files a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png and b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
new file mode 100644
index 0000000000000000000000000000000000000000..c11863ab10c57d456c302e250f7c7df810857e66
Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png differ
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 306caabf6e6fb1dcd0524f90817d65a5dc18a750..c36dfdb78ecdcbd3996b7c2812a6f4f55c586dd3 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,53 +1,118 @@
 # Import your project from GitHub to GitLab
 
+Import your projects from GitHub to GitLab with minimal effort.
+
+## Overview
+
 >**Note:**
-In order to enable the GitHub import setting, you may also want to
-enable the [GitHub integration][gh-import] in your GitLab instance. This
-configuration is optional, you will be able import your GitHub repositories
-with a Personal Access Token.
+If you are an administrator you can enable the [GitHub integration][gh-import]
+in your GitLab instance sitewide. This configuration is optional, users will be
+able import their GitHub repositories with a [personal access token][gh-token].
 
-At its current state, GitHub importer can import:
+- At its current state, GitHub importer can import:
+  - the repository description (GitLab 7.7+)
+  - the Git repository data (GitLab 7.7+)
+  - the issues (GitLab 7.7+)
+  - the pull requests (GitLab 8.4+)
+  - the wiki pages (GitLab 8.4+)
+  - the milestones (GitLab 8.7+)
+  - the labels (GitLab 8.7+)
+  - the release note descriptions (GitLab 8.12+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in GitHub
+  it will be created as private in GitLab as well.
 
-- the repository description (introduced in GitLab 7.7)
-- the git repository data (introduced in GitLab 7.7)
-- the issues (introduced in GitLab 7.7)
-- the pull requests (introduced in GitLab 8.4)
-- the wiki pages (introduced in GitLab 8.4)
-- the milestones (introduced in GitLab 8.7)
-- the labels (introduced in GitLab 8.7)
+## How it works
 
-With GitLab 8.7+, references to pull requests and issues are preserved.
+When issues/pull requests are being imported, the GitHub importer tries to find
+the GitHub author/assignee in GitLab's database using the GitHub ID. For this
+to work, the GitHub author/assignee should have signed in beforehand in GitLab
+and [**associated their GitHub account**][social sign-in]. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original GitHub author is kept.
 
-The importer page is visible when you [create a new project][new-project].
-Click on the **GitHub** link and, if you are logged in via the GitHub
-integration, you will be redirected to GitHub for permission to access your
-projects. After accepting, you'll be automatically redirected to the importer.
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
 
-If you are not using the GitHub integration, you can still perform a one-off
-authorization with GitHub to access your projects.
+## Importing your GitHub repositories
 
-Alternatively, you can also enter a GitHub Personal Access Token. Once you enter
-your token, you'll be taken to the importer.
+The importer page is visible when you create a new project.
 
 ![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
 
----
+Click on the **GitHub** link and the import authorization process will start.
+There are two ways to authorize access to your GitHub repositories:
+
+1. [Using the GitHub integration][gh-integration] (if it's enabled by your
+   GitLab administrator). This is the preferred way as it's possible to
+   preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
+   section.
+1. [Using a personal access token][gh-token] provided by GitHub.
+
+![Select authentication method](img/import_projects_from_github_select_auth_method.png)
+
+### Authorize access to your repositories using the GitHub integration
+
+If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
+you can use it instead of the personal access token.
+
+1. First you may want to connect your GitHub account to GitLab in order for
+   the username mapping to be correct. Follow the [social sign-in] documentation
+   on how to do so.
+1. Once you connect GitHub, click the **List your GitHub repositories** button
+   and you will be redirected to GitHub for permission to access your projects.
+1. After accepting, you'll be automatically redirected to the importer.
+
+You can now go on and [select which repositories to import](#select-which-repositories-to-import).
+
+### Authorize access to your repositories using a personal access token
+
+>**Note:**
+For a proper author/assignee mapping for issues and pull requests, the
+[GitHub integration][gh-integration] should be used instead of the
+[personal access token][gh-token]. If the GitHub integration is enabled by your
+GitLab administrator, it should be the preferred method to import your repositories.
+Read more in the [How it works](#how-it-works) section.
+
+If you are not using the GitHub integration, you can still perform a one-off
+authorization with GitHub to grant GitLab access your repositories:
+
+1. Go to <https://github.com/settings/tokens/new>.
+1. Enter a token description.
+1. Check the `repo` scope.
+1. Click **Generate token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the GitHub importer.
+1. Hit the **List your GitHub repositories** button and wait while GitLab reads
+   your repositories' information. Once done, you'll be taken to the importer
+   page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your GitHub repositories, you will be
+redirected to the GitHub importer page.
+
+From there, you can see the import statuses of your GitHub repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+  right side of the table.
 
-While at the GitHub importer page, you can see the import statuses of your
-GitHub projects. Those that are being imported will show a _started_ status,
-those already imported will be green, whereas those that are not yet imported
-have an **Import** button on the right side of the table. If you want, you can
-import all your GitHub projects in one go by hitting **Import all projects**
-in the upper left corner.
+If you want, you can import all your GitHub projects in one go by hitting
+**Import all projects** in the upper left corner.
 
 ![GitHub importer page](img/import_projects_from_github_importer.png)
 
 ---
 
-The importer will create any new namespaces if they don't exist or in the
-case the namespace is taken, the project will be imported on the user's
-namespace.
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
 
 [gh-import]: ../../integration/github.md "GitHub integration"
-[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
 [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
+[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
+[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 4828bb5dce6c4689cd2caae24a1978696c903320..423b095e69e7558c2b42643e0fe1bd4ccc4e4ff3 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -4,6 +4,112 @@ Subversion (SVN) is a central version control system (VCS) while
 Git is a distributed version control system. There are some major differences
 between the two, for more information consult your favorite search engine.
 
+## Overview
+
+There are two approaches to SVN to Git migration:
+
+1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
+    - Makes the GitLab repository to mirror the SVN project.
+    - Git and SVN repositories are kept in sync; you can use either one.
+    - Smoothens the migration process and allows to manage migration risks.
+
+1. [Cut over migration](#cut-over-migration-with-svn2git) which:
+     - Translates and imports the existing data and history from SVN to Git.
+     - Is a fire and forget approach, good for smaller teams.
+
+## Smooth migration with a Git/SVN mirror using SubGit
+
+[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
+migration. It creates a writable Git mirror of a local or remote Subversion
+repository and that way you can use both Subversion and Git as long as you like.
+It requires access to your GitLab server as it talks with the Git repositories
+directly in a filesystem level.
+
+### SubGit prerequisites
+
+1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
+   follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
+1. Download SubGit from https://subgit.com/download/.
+1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
+   command will be available at `/opt/subgit-VERSION/bin/subgit`.
+
+### SubGit configuration
+
+The first step to mirror you SVN repository in GitLab is to create a new empty
+project which will be used as a mirror. For Omnibus installations the path to
+the repository will be located at
+`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
+installations from source, the default repository directory will be
+`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
+variable:
+
+```
+GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
+```
+
+SubGit will keep this repository in sync with a remote SVN project. For
+convenience, assign your remote SVN project URL to a variable:
+
+```
+SVN_PROJECT_URL=http://svn.company.com/repos/project
+```
+
+Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
+`subgit` command is ran on behalf of the same user that keeps ownership of
+GitLab Git repositories (by default `git`):
+
+```
+subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
+```
+
+Adjust authors and branches mappings, if necessary. Open with your favorite
+text editor:
+
+```
+edit $GIT_REPO_PATH/subgit/authors.txt
+edit $GIT_REPO_PATH/subgit/config
+```
+
+For more information regarding the SubGit configuration options, refer to
+[SubGit's documentation](https://subgit.com/documentation.html) website.
+
+### Initial translation
+
+Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
+initial translation of existing SVN revisions into the Git repository:
+
+```
+subgit install $GIT_REPOS_PATH
+```
+
+After the initial translation is completed, the Git repository and the SVN
+project will be kept in sync by `subgit` - new Git commits will be translated to
+SVN revisions and new SVN revisions will be translated to Git commits. Mirror
+works transparently and does not require any special commands.
+
+If you would prefer to perform one-time cut over migration with `subgit`, use
+the `import` command instead of `install`:
+
+```
+subgit import $GIT_REPO_PATH
+```
+
+### SubGit licensing
+
+Running SubGit in a mirror mode requires a
+[registration](https://subgit.com/pricing.html). Registration is free for open
+source, academic and startup projects.
+
+We're currently working on deeper GitLab/SubGit integration. You may track our
+progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
+
+### SubGit support
+
+For any questions related to SVN to GitLab migration with SubGit, you can
+contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
+
+## Cut over migration with svn2git
+
 If you are currently using an SVN repository, you can migrate the repository
 to Git and GitLab. We recommend a hard cut over - run the migration command once
 and then have all developers start using the new GitLab repository immediately.
@@ -75,5 +181,3 @@ git push --tags origin
 ## Contribute to this guide
 We welcome all contributions that would expand this guide with instructions on
 how to migrate from SVN and other version control systems.
-
-
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 9dc1e9b47e36348e3940d2a6deb4f045e1b9db85..b3c73e947f03510e47c8a82e29b388d3d5edfb1e 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -45,5 +45,5 @@ In `config/gitlab.yml`:
 * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
   is not supported
 * Currently, removing LFS objects from GitLab Git LFS storage is not supported
-* LFS authentications via SSH is not supported for the time being
-* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
+* LFS authentications via SSH was added with GitLab 8.12
+* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
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 9fe065fa68003895c1395b87a0b57f2502d0e18d..1a4f213a792d4eec1d5ada4897a4feb272cefcbe 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,6 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
   credentials store is recommended
 * Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
   to add the URL to Git config manually (see #troubleshooting)
+  
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
 
 ## Using Git LFS
 
@@ -132,6 +136,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
 
 ### Credentials are always required when pushing an object
 
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
+
 Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
 the LFS object on every push for every object, user HTTPS credentials are required.
 
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index d2ec56e650456cf173004b3ceacce8d0d8bfcddc..a68bb8b27ca6b58c33784ad427785bf4922f4535 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -1,63 +1 @@
-# Merge Requests
-
-Merge requests allow you to exchange changes you made to source code
-
-## Only allow merge requests to be merged if the build succeeds
-
-You can prevent merge requests from being merged if their build did not succeed
-in the project settings page.
-
-![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png)
-
-Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box.
-
-Please note that you need to have builds configured to enable this feature.
-
-## Checkout merge requests locally
-
-Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
-
-```
-[remote "origin"]
-  url = https://gitlab.com/gitlab-org/gitlab-ce.git
-  fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section.
-
-It should look like this:
-
-```
-[remote "origin"]
-  url = https://gitlab.com/gitlab-org/gitlab-ce.git
-  fetch = +refs/heads/*:refs/remotes/origin/*
-  fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests requests:
-
-```
-$ git fetch origin
-From https://gitlab.com/gitlab-org/gitlab-ce.git
- * [new ref]         refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref]         refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-To check out a particular merge request:
-
-```
-$ git checkout origin/merge-requests/1
-```
-
-## Ignore whitespace changes in Merge Request diff view
-
-![MR diff](merge_requests/merge_request_diff.png)
-
-If you click the "Hide whitespace changes" button, you can see the diff without whitespace changes.
-
-![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png)
-
-It is also working on commits compare view.
-
-![Commit Compare](merge_requests/commit_compare.png)
+This document was moved to [user/project/merge_requests](../user/project/merge_requests.md).
diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png
deleted file mode 100644
index 3ebbfb75ea38f1157491357f8a93d3ee3b729794..0000000000000000000000000000000000000000
Binary files a/doc/workflow/merge_requests/merge_request_diff.png and /dev/null differ
diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
deleted file mode 100644
index a0db535019c3cc6466646570ee8a1204d82d2c16..0000000000000000000000000000000000000000
Binary files a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png and /dev/null differ
diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md
index 75e1fdff2b2f9f72cfc388bdcbea9adbed9cbb7f..95afd12ebdb95336cb836eeec6755338c0b19efd 100644
--- a/doc/workflow/merge_when_build_succeeds.md
+++ b/doc/workflow/merge_when_build_succeeds.md
@@ -1,15 +1 @@
-# Merge When Build Succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually.
-
-![Enable](merge_when_build_succeeds/enable.png)
-
-When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button.
-
-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](merge_when_build_succeeds/status.png)
-
-When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure.
-
-When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed.
+This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index b4a9c2f3d3e622a837cdc943607261656efecd64..c936e8833c6e0770b377e465109235e41f685d10 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -66,8 +66,9 @@ Below is the table of events users can be notified of:
 In all of the below cases, the notification will be sent to:
 - Participants:
   - the author and assignee of the issue/merge request
+  - the author of the pipeline
   - authors of comments on the issue/merge request
-  - anyone mentioned by `@username` in the issue/merge request description
+  - anyone mentioned by `@username` in the issue/merge request title or description
   - anyone mentioned by `@username` in any of the comments on the issue/merge request
 
     ...with notification level "Participating" or higher
@@ -88,6 +89,13 @@ In all of the below cases, the notification will be sent to:
 | Reopen merge request   | |
 | Merge merge request    | |
 | New comment            | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+| Failed pipeline        | The above, plus the author of the pipeline |
+| Successful pipeline    | The above, plus the author of the pipeline |
+
+
+In addition, if the title or description of an Issue or Merge Request is
+changed, notifications will be sent to any **new** mentions by `@username` as
+if they had been mentioned in the original text.
 
 You won't receive notifications for Issues, Merge Requests or Milestones
 created by yourself. You will only receive automatic notifications when
diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md
index a523b3facbe317030977849f3e15c18d723309b7..f19e7df8c9aa59fcf4b4c8a04c124ea4ac7db2a9 100644
--- a/doc/workflow/project_features.md
+++ b/doc/workflow/project_features.md
@@ -32,4 +32,12 @@ Snippets are little bits of code or text.
 
 This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control.
 
-For example, a specific config file that is used by > the team that is only valid for the people that work on the code.
+For example, a specific config file that is used by the team that is only valid for the people that work on the code.
+
+## Git LFS
+
+>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins.
+
+Git Large File Storage allows you to easily manage large binary files with Git.
+With this setting admins can better control which projects are allowed to use 
+LFS. 
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
index 5ead9f4177f2bb35a85a1a971330b1be64d61772..cf1292253fcf6aea1ce485adbea4d1e4b9d081fe 100644
--- a/doc/workflow/revert_changes.md
+++ b/doc/workflow/revert_changes.md
@@ -1,64 +1 @@
-# Reverting changes
-
-> [Introduced][ce-1990] in GitLab 8.5.
-
----
-
-GitLab implements Git's powerful feature to [revert any commit][git-revert]
-with introducing a **Revert** button in Merge Requests and commit details.
-
-## Reverting a Merge Request
-
-_**Note:** The **Revert** button will only be available for Merge Requests
-created since GitLab 8.5. However, you can still revert a Merge Request
-by reverting the merge commit from the list of Commits page._
-
-After the Merge Request has been merged, a **Revert** button will be available
-to revert the changes introduced by that Merge Request:
-
-![Revert Merge Request](img/revert_changes_mr.png)
-
----
-
-You can revert the changes directly into the selected branch or you can opt to
-create a new Merge Request with the revert changes:
-
-![Revert Merge Request modal](img/revert_changes_mr_modal.png)
-
----
-
-After the Merge Request has been reverted, the **Revert** button will not be
-available anymore.
-
-## Reverting a Commit
-
-You can revert a Commit from the Commit details page:
-
-![Revert commit](img/revert_changes_commit.png)
-
----
-
-Similar to reverting a Merge Request, you can opt to revert the changes
-directly into the target branch or create a new Merge Request to revert the
-changes:
-
-![Revert commit modal](img/revert_changes_commit_modal.png)
-
----
-
-After the Commit has been reverted, the **Revert** button will not be available
-anymore.
-
-Please note that when reverting merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to revert a merge commit using the second parent as the
-mainline:
-
-```bash
-git revert -m 2 7a39eb0
-```
-
-[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
-[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
+This document was moved to [user/project/merge_requests/revert_changes](../user/project/merge_requests/revert_changes.md).
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 4c59f59c5871b094614386a86a99ffd12fe21ad4..8e50cb03e638f49ec9c44d9419b28a8e56d7be1f 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,22 +1,24 @@
 # Share Projects with other Groups
 
-In GitLab Enterprise Edition you can share projects with other groups.
-This makes it possible to add a group of users to a project with a single action.
+You can share projects with other groups. This makes it possible to add a group of users
+to a project with a single action.
 
 ## Groups as collections of users
 
-In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
-In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+Groups are used primarily to [create collections of projects](groups.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
 
 ## Sharing a project with a group of users
 
-The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
-But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the (Enterprise Edition only) group sharing feature can be of use.
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'.  But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
 
 To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
 
-![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
 
 Now you can add the 'Engineering' group with the maximum access level of your choice.
 After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index ee8e78625727bd8d51a835c6a2852492a22c3326..595c7da155b06c4262a549702aa3b98669a849bc 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -1,151 +1 @@
-# GitLab Web Editor
-
-Sometimes it's easier to make quick changes directly from the GitLab interface
-than to clone the project and use the Git command line tool. In this feature
-highlight we look at how you can create a new file, directory, branch or
-tag from the file browser. All of these actions are available from a single
-dropdown menu.
-
-## Create a file
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New file** from the dropdown.
-
-![New file dropdown menu](img/web_editor_new_file_dropdown.png)
-
----
-
-Enter a file name in the **File name** box. Then, add file content in the editor
-area. Add a descriptive commit message and choose a branch. The branch field
-will default to the branch you were viewing in the file browser. If you enter
-a new branch name, a checkbox will appear allowing you to start a new merge
-request after you commit the changes.
-
-When you are satisfied with your new file, click **Commit Changes** at the bottom.
-
-![Create file editor](img/web_editor_new_file_editor.png)
-
-## Upload a file
-
-The ability to create a file is great when the content is text. However, this
-doesn't work well for binary data such as images, PDFs or other file types. In
-this case you need to upload a file.
-
-From a project's files page, click the '+' button to the right of the branch
-selector. Choose **Upload file** from the dropdown.
-
-![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
-
----
-
-Once the upload dialog pops up there are two ways to upload your file. Either
-drag and drop a file on the pop up or use the **click to upload** link. A file
-preview will appear once you have selected a file to upload.
-
-Enter a commit message, choose a branch, and click **Upload file** when you are
-ready.
-
-![Upload file dialog](img/web_editor_upload_file_dialog.png)
-
-## Create a directory
-
-To keep files in the repository organized it is often helpful to create a new
-directory.
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New directory** from the dropdown.
-
-![New directory dropdown](img/web_editor_new_directory_dropdown.png)
-
----
-
-In the new directory dialog enter a directory name, a commit message and choose
-the target branch. Click **Create directory** to finish.
-
-![New directory dialog](img/web_editor_new_directory_dialog.png)
-
-## Create a new branch
-
-There are multiple ways to create a branch from GitLab's web interface.
-
-### Create a new branch from an issue
-
-> [Introduced][ce-2808] in GitLab 8.6.
-
-In case your development workflow dictates to have an issue for every merge
-request, you can quickly create a branch right on the issue page which will be
-tied with the issue itself. You can see a **New Branch** button after the issue
-description, unless there is already a branch with the same name or a referenced
-merge request.
-
-![New Branch Button](img/new_branch_from_issue.png)
-
-Once you click it, a new branch will be created that diverges from the default
-branch of your project, by default `master`. The branch name will be based on
-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`.
-
-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.
-
-### Create a new branch from a project's dashboard
-
-If you want to make changes to several files before creating a new merge
-request, you can create a new branch up front. From a project's files page,
-choose **New branch** from the dropdown.
-
-![New branch dropdown](img/web_editor_new_branch_dropdown.png)
-
----
-
-Enter a new **Branch name**. Optionally, change the **Create from** field
-to choose which branch, tag or commit SHA this new branch will originate from.
-This field will autocomplete if you start typing an existing branch or tag.
-Click **Create branch** and you will be returned to the file browser on this new
-branch.
-
-![New branch page](img/web_editor_new_branch_page.png)
-
----
-
-You can now make changes to any files, as needed. When you're ready to merge
-the changes back to master you can use the widget at the top of the screen.
-This widget only appears for a period of time after you create the branch or
-modify files.
-
-![New push widget](img/web_editor_new_push_widget.png)
-
-## Create a new tag
-
-Tags are useful for marking major milestones such as production releases,
-release candidates, and more. You can create a tag from a branch or a commit
-SHA. From a project's files page, choose **New tag** from the dropdown.
-
-![New tag dropdown](img/web_editor_new_tag_dropdown.png)
-
----
-
-Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
-would like to create this new tag. You can optionally add a message and
-release notes. The release notes section supports markdown format and you can
-also upload an attachment. Click **Create tag** and you will be taken to the tag
-list page.
-
-![New tag page](img/web_editor_new_tag_page.png)
-
-## Tips
-
-When creating or uploading a new file, or creating a new directory, you can
-trigger a new merge request rather than committing directly to master. Enter
-a new branch name in the **Target branch** field. You will notice a checkbox
-appear that is labeled **Start a new merge request with these changes**. After
-you commit the changes you will be taken to a new merge request form.
-
-![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-
-[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../customization/issue_closing.md
+This document was moved to [user/project/repository/web_editor](../user/project/repository/web_editor.md).
diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md
index 46035a5e6b68db8a623b28e7ee6c378a2694ca3c..abb8002f442e85441a47998abe7ac14b2c251ead 100644
--- a/doc/workflow/wip_merge_requests.md
+++ b/doc/workflow/wip_merge_requests.md
@@ -1,13 +1 @@
-# "Work In Progress" Merge Requests
-
-To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**.
-
-![Blocked Accept Button](wip_merge_requests/blocked_accept_button.png)
-
-To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`.
-
-![Mark as WIP](wip_merge_requests/mark_as_wip.png)
-
-To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix.
-
-![Unark as WIP](wip_merge_requests/unmark_as_wip.png)
+This document was moved to [user/project/merge_requests/work_in_progress_merge_requests](../user/project/merge_requests/work_in_progress_merge_requests.md).
diff --git a/docker/README.md b/docker/README.md
index ee1f32adc26f9bf26719806598e84931dfa47efc..f9e12c5733b85098de0f41c5a5980ecdd18ac05a 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -2,6 +2,6 @@
 
 * The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/).
 * The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
-* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/)
+* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
 * The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
-* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#build-docker-image)
+* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
index 08b87808f337eac54d8b70809af9b78a532b5f5b..bd883a0ebfafb09e6838fd4968cbb26eba43f34d 100644
--- a/features/dashboard/active_tab.feature
+++ b/features/dashboard/active_tab.feature
@@ -18,7 +18,7 @@ Feature: Dashboard Active Tab
     Then the active main tab should be Merge Requests
     And no other main tabs should be active
 
-  Scenario: On Dashboard Help
-    Given I visit dashboard help page
-    Then the active main tab should be Help
+  Scenario: On Dashboard Groups
+    Given I visit dashboard groups page
+    Then the active main tab should be Groups
     And no other main tabs should be active
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 1f4c9020731c688005372ef96648378983ffc431..92061dac7f446ba25839cbe791c136b74be935f8 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,7 +11,6 @@ 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
 
@@ -31,19 +30,6 @@ Feature: Dashboard
     And I click "Create Merge Request" link
     Then I see prefilled new Merge Request page
 
-  @javascript
-  Scenario: I should see User joined Project event
-    Given user with name "John Doe" joined project "Shop"
-    When I visit dashboard activity page
-    Then I should see "John Doe joined project Shop" event
-
-  @javascript
-  Scenario: I should see User left Project event
-    Given user with name "John Doe" joined project "Shop"
-    And user with name "John Doe" left project "Shop"
-    When I visit dashboard activity page
-    Then I should see "John Doe left project Shop" event
-
   @javascript
   Scenario: Sorting Issues
     Given I visit dashboard issues page
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
index 42f5d6d2af7f5357b9f33677a587a49c5e41580e..0b23bbb7951d822562206cff658b863b30c0b10a 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -22,26 +22,6 @@ Feature: Dashboard Todos
     And I mark all todos as done
     Then I should see all todos marked as done
 
-  @javascript
-    Scenario: I filter by project
-      Given I filter by "Enterprise"
-      Then I should not see todos
-
-  @javascript
-    Scenario: I filter by author
-      Given I filter by "John Doe"
-      Then I should not see todos related to "Mary Jane" in the list
-
-  @javascript
-    Scenario: I filter by type
-      Given I filter by "Issue"
-      Then I should not see todos related to "Merge Requests" in the list
-
-  @javascript
-    Scenario: I filter by action
-      Given I filter by "Mentioned"
-      Then I should not see todos related to "Assignments" in the list
-
   @javascript
     Scenario: I click on a todo row
       Given I click on the todo
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
index 092e18d1b8697f0ad720aa9626e1ef14541c4366..4e0f4486ab7ac26c302e3c6bfdc03202374c792a 100644
--- a/features/explore/projects.feature
+++ b/features/explore/projects.feature
@@ -128,6 +128,7 @@ Feature: Explore Projects
     And project "Archive" has comments
     And I sign in as a user
     And project "Community" has comments
+    And trending projects are refreshed
     When I visit the explore trending projects
     Then I should see project "Community"
     And I should not see project "Internal"
diff --git a/features/groups.feature b/features/groups.feature
index 49e939807b5258addbd7494e4ff843794516af25..4044bd9be79c93a142b414283b1b465c4234cdbb 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -39,11 +39,6 @@ Feature: Groups
     When I visit group "Owned" merge requests page
     Then I should not see merge requests from the archived project
 
-  Scenario: I should see edit group "Owned" page
-    When I visit group "Owned" settings page
-    And I change group "Owned" name to "new-name"
-    Then I should see new group "Owned" name
-
   Scenario: I edit group "Owned" avatar
     When I visit group "Owned" settings page
     And I change group "Owned" avatar
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 447dd92a458b23a963d120a5cffcdb386f20fa0e..dc1339deb4c5c0636827a97ec10db8d74d1e1079 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -59,11 +59,6 @@ Feature: Profile
     When I unsuccessfully change my password
     Then I should see a password error message
 
-  Scenario: I reset my token
-    Given I visit profile account page
-    Then I reset my token
-    And I should see new token
-
   Scenario: I visit history tab
     Given I have activity
     When I visit Audit Log page
diff --git a/features/profile/ssh_keys.feature b/features/profile/ssh_keys.feature
deleted file mode 100644
index b0d5b7489165554d7c8666c2f0e8a29248acbe9a..0000000000000000000000000000000000000000
--- a/features/profile/ssh_keys.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@profile
-Feature: Profile SSH Keys
-  Background:
-    Given I sign in as a user
-    And I have ssh key "ssh-rsa Work"
-    And I visit profile keys page
-
-  Scenario: I should see ssh keys
-    Then I should see my ssh keys
-
-  Scenario: Add new ssh key
-    Given I should see new ssh key form
-    And I submit new ssh key "Laptop"
-    Then I should see new ssh key "Laptop"
-
-  Scenario: Remove ssh key
-    Given I click link "Work"
-    And I click link "Remove"
-    Then I visit profile keys page
-    And I should not see "Work" ssh key
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 2c17d32154a9788bcb168213358aa305e506c154..88fef674c0cf7c67450a36335556b130f3f70f6e 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -22,6 +22,7 @@ Feature: Project Commits Branches
   @javascript
   Scenario: I delete a branch
     Given I visit project branches page
+    And I filter for branch improve/awesome
     And I click branch 'improve/awesome' delete link
     Then I should not see branch 'improve/awesome'
 
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 8b0cb90765eccd30808ac6dfcbd1938db6cec970..1776c07e60e1d32245bd9e9a7ea368a563a40cfc 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -37,6 +37,11 @@ Feature: Project Commits
     Then I see commit info
     And I see side-by-side diff button
 
+  Scenario: I browse commit from list and create a new tag
+    Given I click on commit link
+    And I click on tag link
+    Then I see commit SHA pre-filled
+
   Scenario: I browse commit with ci from list
     Given commit has ci status
     And repository contains ".gitlab-ci.yml" file
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 358e622b73686afb5bbba1b560257be4932d67f4..80670063ea00e0b89269d17ce88b3f35f098967b 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -37,6 +37,7 @@ Feature: Project Issues
     And I submit new issue "500 error on profile"
     Then I should see issue "500 error on profile"
 
+  @javascript
   Scenario: I submit new unassigned issue with labels
     Given project "Shop" has labels: "bug", "feature", "enhancement"
     And I click link "New Issue"
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 6bac601146764a201d1d60c1aee85980910e471c..5aa592e9067ccc9d8cc9f49af8c0e46535b06ad8 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -24,7 +24,7 @@ Feature: Project Merge Requests
   Scenario: I should see target branch when it is different from default
     Given project "Shop" have "Bug NS-06" open merge request
     When I visit project "Shop" merge requests page
-    Then I should see "other_branch" branch
+    Then I should see "feature_conflict" branch
 
   Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
     Given project "Shop" have "Bug NS-07" open merge request with rebased branch
@@ -89,7 +89,7 @@ Feature: Project Merge Requests
     Then The list should be sorted by "Oldest updated"
 
   @javascript
-  Scenario: Visiting Merge Requests from a differente Project after sorting
+  Scenario: Visiting Merge Requests from a different Project after sorting
     Given I visit project "Shop" merge requests page
     And I sort the list by "Oldest updated"
     And I visit dashboard merge requests page
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 89a02706bd283c04fe90fd46b7c2bd8d2d712314..93c884e23c58c451f6dba531f1fe59fa234cec5f 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -43,4 +43,4 @@ Feature: Project Network Graph
 
   Scenario: I should fail to look for a commit
     When I look for a commit by ";"
-    Then page status code should be 404
+    Then I should see non-existent git revision error message
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 270557cbde7adfb8e8a250308bf6c6dd1a600ed7..3c51ea56585e23a8736b03a93eea447f16b5f2f3 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -12,7 +12,7 @@ Feature: Project Snippets
     And I should not see "Snippet two" in snippets
 
   Scenario: I create new project snippet
-    Given I click link "New Snippet"
+    Given I click link "New snippet"
     And I submit new snippet "Snippet three"
     Then I should see snippet "Snippet three"
 
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index fdffd71de85f85f826c9d610c1a2e33dc1db92bb..d4b91fec6e831c6427d8581f39be5ddba49518ab 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -71,6 +71,7 @@ Feature: Project Source Browse Files
     And I fill the new branch name
     And I click on "Commit Changes"
     Then I am redirected to the new merge request page
+    When I click on "Changes" tab
     And I should see its new content
 
   @javascript
@@ -80,9 +81,10 @@ Feature: Project Source Browse Files
     And I fill the upload file commit message
     And I fill the new branch name
     And I click on "Upload file"
-    Then I can see the new text file
+    Then I can see the new commit message
     And I am redirected to the new merge request page
-    And I can see the new commit message
+    When I click on "Changes" tab
+    Then I can see the new text file
 
   @javascript
   Scenario: I can upload file and commit when I don't have write access
@@ -93,9 +95,10 @@ Feature: Project Source Browse Files
     And I upload a new text file
     And I fill the upload file commit message
     And I click on "Upload file"
-    Then I can see the new text file
+    Then I can see the new commit message
     And I am redirected to the fork's new merge request page
-    And I can see the new commit message
+    When I click on "Changes" tab
+    Then I can see the new text file
 
   @javascript
   Scenario: I can replace file and commit
@@ -119,9 +122,10 @@ Feature: Project Source Browse Files
     And I replace it with a text file
     And I fill the replace file commit message
     And I click on "Replace file"
-    Then I can see the new text file
-    And I am redirected to the fork's new merge request page
     And I can see the replacement commit message
+    And I am redirected to the fork's new merge request page
+    When I click on "Changes" tab
+    Then I can see the new text file
 
   @javascript
   Scenario: If I enter an illegal file name I see an error message
@@ -191,6 +195,7 @@ Feature: Project Source Browse Files
     And I fill the new branch name
     And I click on "Commit Changes"
     Then I am redirected to the new merge request page
+    Then I click on "Changes" tab
     And I should see its new content
 
   @javascript  @wip
diff --git a/features/project/source/git_blame.feature b/features/project/source/git_blame.feature
deleted file mode 100644
index 48b1077dc6b8f5d28cb633d72ce4ca7b732e69c3..0000000000000000000000000000000000000000
--- a/features/project/source/git_blame.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: Project Source Git Blame
-  Background:
-    Given I sign in as a user
-    And I own project "Shop"
-    Given I visit project source page
-
-  Scenario: I blame file
-    Given I click on ".gitignore" file in repo
-    And I click Blame button
-    Then I should see git file blame
diff --git a/features/snippets/public_snippets.feature b/features/snippets/public_snippets.feature
deleted file mode 100644
index c2afb63b6d894917cb90be8fe5a18faa193420f3..0000000000000000000000000000000000000000
--- a/features/snippets/public_snippets.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: Public snippets
-  Scenario: Unauthenticated user should see public snippets
-    Given There is public "Personal snippet one" snippet
-    And I visit snippet page "Personal snippet one"
-    Then I should see snippet "Personal snippet one"
-
-  Scenario: Unauthenticated user should see raw public snippets
-    Given There is public "Personal snippet one" snippet
-    And I visit snippet raw page "Personal snippet one"
-    Then I should see raw snippet "Personal snippet one"
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 0c89a3db9ad1f0e09b5f44e285b135fba035824f..9396a76f0a288c7857433dc555f03b58562ac3cd 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -105,7 +105,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
       select "Developer", from: "access_level"
     end
 
-    click_button "Add users to group"
+    click_button "Add to group"
   end
 
   step 'I should see current user as "Developer"' do
diff --git a/features/steps/admin/logs.rb b/features/steps/admin/logs.rb
index f9e49588c7541c9ffddba14b2171ce70201d3a27..63881d69146fb708e08680fa4b212d44f1cd3cae 100644
--- a/features/steps/admin/logs.rb
+++ b/features/steps/admin/logs.rb
@@ -4,7 +4,7 @@ class Spinach::Features::AdminLogs < Spinach::FeatureSteps
   include SharedAdmin
 
   step 'I should see tabs with available logs' do
-    expect(page).to have_content 'production.log'
+    expect(page).to have_content 'test.log'
     expect(page).to have_content 'githost.log'
     expect(page).to have_content 'application.log'
   end
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
index d77945a6b9cd46b4ecb0a0bf33b5e52a021aaf37..2b8cd030acef899b1a5e4a2ad556c2516e5cd8cd 100644
--- a/features/steps/admin/projects.rb
+++ b/features/steps/admin/projects.rb
@@ -70,7 +70,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
       select "Developer", from: "access_level"
     end
 
-    click_button "Add users to project"
+    click_button "Add to project"
   end
 
   step 'I should see current user as "Developer"' do
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 03f87df7a60cf02657e813000516fea7dacef500..11dc7f580f03293119157ba1a3cd72744d8907de 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
     page.check('Issue')
     page.check('Merge request')
     page.check('Build')
+    page.check('Pipeline')
     click_on 'Save'
   end
 
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index a7d61bc28e0a639a6ed8412638ff90689abba5be..b2bec369e0f8e29d90581b4442be88ff061db666 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -33,33 +33,6 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
     expect(find("input#merge_request_target_branch").value).to eq "master"
   end
 
-  step 'user with name "John Doe" joined project "Shop"' do
-    user = create(:user, { name: "John Doe" })
-    project.team << [user, :master]
-    Event.create(
-      project: project,
-      author_id: user.id,
-      action: Event::JOINED
-    )
-  end
-
-  step 'I should see "John Doe joined project Shop" event' do
-    expect(page).to have_content "John Doe joined project #{project.name_with_namespace}"
-  end
-
-  step 'user with name "John Doe" left project "Shop"' do
-    user = User.find_by(name: "John Doe")
-    Event.create(
-      project: project,
-      author_id: user.id,
-      action: Event::LEFT
-    )
-  end
-
-  step 'I should see "John Doe left project Shop" event' do
-    expect(page).to have_content "John Doe left project #{project.name_with_namespace}"
-  end
-
   step 'I have group with projects' do
     @group   = create(:group)
     @project = create(:project, namespace: @group)
diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb
index 9c94dc70df0bda915ec42b9d9e3cd0366c265dff..3c5bf44c538bb722bd64335f790412b7702f9ba3 100644
--- a/features/steps/dashboard/help.rb
+++ b/features/steps/dashboard/help.rb
@@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
   end
 
   step 'I visit the "Rake Tasks" help page' do
-    visit help_page_path("raketasks/maintenance")
+    visit help_page_path("administration/raketasks/maintenance")
   end
 
   step 'I should see "Rake Tasks" page markdown rendered' do
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index f0d8d498e4623b7e220621ca102f5adec1c0abf0..cb36d6ae1a98b80d7f99f2822947bbb16ecc6950 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -18,9 +18,9 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
     expect(page).to have_link('GitHub')
     expect(page).to have_link('Bitbucket')
     expect(page).to have_link('GitLab.com')
-    expect(page).to have_link('Gitorious.org')
     expect(page).to have_link('Google Code')
     expect(page).to have_link('Repo by URL')
+    expect(page).to have_link('GitLab export')
   end
 
   step 'I click on "Import project from GitHub"' do
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 60152d3da55ff49e0babaec939fb0bfcd5af5bd3..344b6fda9a638f941dd44879ca33eab04f20ddf2 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -3,7 +3,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
   include SharedPaths
   include SharedProject
   include SharedUser
-  include Select2Helper
 
   step '"John Doe" is a developer of project "Shop"' do
     project.team << [john_doe, :developer]
@@ -54,7 +53,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
     page.within('.todos-pending-count') { expect(page).to have_content '0' }
     expect(page).to have_content 'To do 0'
     expect(page).to have_content 'Done 4'
-    expect(page).not_to have_link project.name_with_namespace
+    expect(page).to have_content "You're all done!"
+    expect('.prepend-top-default').not_to have_link project.name_with_namespace
     should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
     should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
     should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
@@ -79,19 +79,31 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
   end
 
   step 'I filter by "Enterprise"' do
-    select2(enterprise.id, from: "#project_id")
+    click_button 'Project'
+    page.within '.dropdown-menu-project' do
+      click_link enterprise.name_with_namespace
+    end
   end
 
   step 'I filter by "John Doe"' do
-    select2(john_doe.id, from: "#author_id")
+    click_button 'Author'
+    page.within '.dropdown-menu-author' do
+      click_link john_doe.username
+    end
   end
 
   step 'I filter by "Issue"' do
-    select2('Issue', from: "#type")
+    click_button 'Type'
+    page.within '.dropdown-menu-type' do
+      click_link 'Issue'
+    end
   end
 
   step 'I filter by "Mentioned"' do
-    select2("#{Todo::MENTIONED}", from: '#action_id')
+    click_button 'Action'
+    page.within '.dropdown-menu-action' do
+      click_link 'Mentioned'
+    end
   end
 
   step 'I should not see todos' do
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def097b4a4fdf0c2b62827a33c8dc810..cefc55d07abef198a32cf024f6302b61557eb3f4 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -1,4 +1,5 @@
 class Spinach::Features::GroupMembers < Spinach::FeatureSteps
+  include WaitForAjax
   include SharedAuthentication
   include SharedPaths
   include SharedGroup
@@ -13,7 +14,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
       select "Reporter", from: "access_level"
     end
 
-    click_button "Add users to group"
+    click_button "Add to group"
   end
 
   step 'I select "Mike" as "Master"' do
@@ -24,7 +25,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
       select "Master", from: "access_level"
     end
 
-    click_button "Add users to group"
+    click_button "Add to group"
   end
 
   step 'I should see "Mike" in team list as "Reporter"' do
@@ -47,7 +48,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
       select "Reporter", from: "access_level"
     end
 
-    click_button "Add users to group"
+    click_button "Add to group"
   end
 
   step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
@@ -66,7 +67,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
       select "Reporter", from: "access_level"
     end
 
-    click_button "Add users to group"
+    click_button "Add to group"
   end
 
   step 'I should see user "John Doe" in team list' do
@@ -108,7 +109,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
   step 'I search for \'Mary\' member' do
     page.within '.member-search-form' do
       fill_in 'search', with: 'Mary'
-      click_button 'Search'
+      find('.member-search-btn').click
     end
   end
 
@@ -116,9 +117,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
     member = mary_jane_member
 
     page.within "#group_member_#{member.id}" do
-      click_button "Edit access level"
-      select 'Developer', from: 'group_member_access_level'
-      click_on 'Save'
+      select 'Developer', from: "member_access_level_#{member.id}"
+      wait_for_ajax
     end
   end
 
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 4fa7d7c656775b03cc8669ae6b6593a9bf723d26..0c88838767cdc33d8ac8f6f0795264fad3c566ea 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -73,18 +73,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
       author: current_user
   end
 
-  step 'I change group "Owned" name to "new-name"' do
-    fill_in 'group_name', with: 'new-name'
-    fill_in 'group_path', with: 'new-name'
-    click_button "Save group"
-  end
-
-  step 'I should see new group "Owned" name' do
-    page.within ".navbar-gitlab" do
-      expect(page).to have_content "new-name"
-    end
-  end
-
   step 'I change group "Owned" avatar' do
     attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
     click_button "Save group"
@@ -129,7 +117,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
   end
 
   step 'I visit group "NonExistentGroup" page' do
-    visit group_path(-1)
+    visit group_path("NonExistentGroup")
   end
 
   step 'the archived project have some issues' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 4ee6784a086f7d3831ebca4f2433437ac9cebf7c..ea480d2ad68d7fe8299b53119278e0aa40b16e2a 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -13,6 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
     fill_in 'user_website_url', with: 'testurl'
     fill_in 'user_location', with: 'Ukraine'
     fill_in 'user_bio', with: 'I <3 GitLab'
+    fill_in 'user_organization', with: 'GitLab'
     click_button 'Update profile settings'
     @user.reload
   end
@@ -23,6 +24,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
     expect(@user.twitter).to eq 'testtwitter'
     expect(@user.website_url).to eq 'testurl'
     expect(@user.bio).to eq 'I <3 GitLab'
+    expect(@user.organization).to eq 'GitLab'
     expect(find('#user_location').value).to eq 'Ukraine'
   end
 
@@ -102,18 +104,6 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
     end
   end
 
-  step 'I reset my token' do
-    page.within '.private-token' do
-      @old_token = @user.private_token
-      click_button "Reset private token"
-    end
-  end
-
-  step 'I should see new token' do
-    expect(find("#token").value).not_to eq @old_token
-    expect(find("#token").value).to eq @user.reload.private_token
-  end
-
   step 'I have activity' do
     create(:closed_issue_event, author: current_user)
   end
diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb
deleted file mode 100644
index a400488a5326bc4ad0bca77f2e7dff31254f8bd0..0000000000000000000000000000000000000000
--- a/features/steps/profile/ssh_keys.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps
-  include SharedAuthentication
-
-  step 'I should see my ssh keys' do
-    @user.keys.each do |key|
-      expect(page).to have_content(key.title)
-    end
-  end
-
-  step 'I should see new ssh key form' do
-    expect(page).to have_content("Add an SSH key")
-  end
-
-  step 'I submit new ssh key "Laptop"' do
-    fill_in "key_title", with: "Laptop"
-    fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
-    click_button "Add key"
-  end
-
-  step 'I should see new ssh key "Laptop"' do
-    key = Key.find_by(title: "Laptop")
-    expect(page).to have_content(key.title)
-    expect(page).to have_content(key.key)
-    expect(current_path).to eq profile_key_path(key)
-  end
-
-  step 'I click link "Work"' do
-    click_link "Work"
-  end
-
-  step 'I click link "Remove"' do
-    click_link "Remove"
-  end
-
-  step 'I visit profile keys page' do
-    visit profile_keys_path
-  end
-
-  step 'I should not see "Work" ssh key' do
-    expect(page).not_to have_content "Work"
-  end
-
-  step 'I have ssh key "ssh-rsa Work"' do
-    create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work")
-  end
-end
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 80043463188d568d438c7c65d6697908f4bb4f3e..58225032859455d9c99fd7e72cab4e918fbf0daa 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -54,7 +54,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
   end
 
   step 'I click the "Branches" tab' do
-    page.within '.content' do
+    page.within '.sub-nav' do
       click_link('Branches')
     end
   end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 4bfb7e92e99657f01c22b45170ca1a2ce3a2c185..5f9b9e0445e85970786f10a770eb363eeb5543d7 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -73,6 +73,11 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
     expect(page).to have_content 'Branch already exists'
   end
 
+  step 'I filter for branch improve/awesome' do
+    fill_in 'branch-search', with: 'improve/awesome'
+    find('#branch-search').native.send_keys(:enter)
+  end
+
   step "I click branch 'improve/awesome' delete link" do
     page.within '.js-branch-improve\/awesome' do
       find('.btn-remove').click
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index bea9f9d198b5b0b34e5a5161de8048227f095838..007dfb67a77641cede3fd6280cb58b9c7b0d4e36 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -21,7 +21,15 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
     expect(response_headers['Content-Type']).to have_content("application/atom+xml")
     expect(body).to have_selector("title", text: "#{@project.name}:master commits")
     expect(body).to have_selector("author email", text: commit.author_email)
-    expect(body).to have_selector("entry summary", text: commit.description[0..10])
+    expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r"))
+  end
+
+  step 'I click on tag link' do
+    click_link "Tag"
+  end
+
+  step 'I see commit SHA pre-filled' do
+    expect(page).to have_selector("input[value='#{sample_commit.id}']")
   end
 
   step 'I click on commit link' do
@@ -34,15 +42,16 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
   end
 
   step 'I fill compare fields with branches' do
-    fill_in 'from', with: 'feature'
-    fill_in 'to',   with: 'master'
+    select_using_dropdown('from', 'feature')
+    select_using_dropdown('to', 'master')
 
     click_button 'Compare'
   end
 
   step 'I fill compare fields with refs' do
-    fill_in "from", with: sample_commit.parent_id
-    fill_in "to",   with: sample_commit.id
+    select_using_dropdown('from', sample_commit.parent_id, true)
+    select_using_dropdown('to', sample_commit.id, true)
+
     click_button "Compare"
   end
 
@@ -89,8 +98,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
   end
 
   step 'I fill compare fields with branches' do
-    fill_in 'from', with: 'master'
-    fill_in 'to',   with: 'feature'
+    select_using_dropdown('from', 'master')
+    select_using_dropdown('to', 'feature')
 
     click_button 'Compare'
   end
@@ -154,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
   end
 
   step 'I see commit ci info' do
-    expect(page).to have_content "Builds for 1 pipeline pending"
+    expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
   end
 
   step 'I click status link' do
@@ -162,7 +171,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
   end
 
   step 'I see builds list' do
-    expect(page).to have_content "Builds for 1 pipeline pending"
+    expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
     expect(page).to have_content "1 build"
   end
 
@@ -174,4 +183,15 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
     expect(page).to have_content "More submodules"
     expect(page).not_to have_content "Change some files"
   end
+
+  def select_using_dropdown(dropdown_type, selection, is_commit = false)
+    dropdown = find(".js-compare-#{dropdown_type}-dropdown")
+    dropdown.find(".compare-dropdown-toggle").click
+    dropdown.fill_in("Filter by Git revision", with: selection)
+    if is_commit
+      dropdown.find('input[type="search"]').send_keys(:return)
+    else
+      find_link(selection, visible: true).click
+    end
+  end
 end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 8abeb5ee24202020c5f2c185f90b3a836c2c4b52..70dbd030003d61677026e0cc2ba2dd8c18f091d9 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
 
   step 'There is an existent fork of the "Shop" project' do
     user = create(:user, name: 'Mike')
+    @project.team << [user, :reporter]
     @forked_project = Projects::ForkService.new(@project, user).execute
   end
 
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index dacab6c79775103fe004a2a5e0dc79b41f9e339e..6c14d83500401e817f0e41ae2b2406bf03ef60c7 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -138,19 +138,19 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
   end
 
   step 'I click "Assign to" dropdown"' do
-    first('.ajax-users-select').click
+    click_button 'Assignee'
   end
 
   step 'I should see the target project ID in the input selector' do
-    expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]")
+    expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}"
   end
 
   step 'I should see the users from the target project ID' do
-    expect(page).to have_selector('.user-result', visible: true, count: 3)
-    users = page.all('.user-name')
-    expect(users[0].text).to eq 'Unassigned'
-    expect(users[1].text).to eq current_user.name
-    expect(users[2].text).to eq @project.users.first.name
+    page.within '.dropdown-menu-user' do
+      expect(page).to have_content 'Unassigned'
+      expect(page).to have_content current_user.name
+      expect(page).to have_content @project.users.first.name
+    end
   end
 
   # Verify a link is generated against the correct project
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index b09ec86e5dfa7af77c1334f9b12777f92b0086ac..7490d2bc6e72ede7c6df518217530f9797acf5a4 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -19,8 +19,8 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
   end
 
   step 'page should have languages graphs' do
-    expect(page).to have_content "Ruby 66.63 %"
-    expect(page).to have_content "JavaScript 22.96 %"
+    expect(page).to have_content /Ruby 66.* %/
+    expect(page).to have_content /JavaScript 22.* %/
   end
 
   step 'page should have commits graphs' do
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1498f899cf588f2531d04f2fde4d354d8445838f..cbe5738e7e4382cedf2c98789f05757c4bdcc796 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
     page.within '.awards' do
       expect(page).to have_selector '.js-emoji-btn'
       expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
-      expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
+      expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
     end
   end
 
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index daee90b3767d8689d573916e6614ed081b748e6e..b50f5238e80e2f3a81c4a1d340ce58af89210065 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -45,6 +45,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
 
   step 'I click link "All"' do
     click_link "All"
+    # Waits for load
+    expect(find('.issues-state-filters > .active')).to have_content 'All'
   end
 
   step 'I click link "Release 0.4"' do
@@ -82,7 +84,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
 
   step 'I submit new issue "500 error on profile" with label \'bug\'' do
     fill_in "issue_title", with: "500 error on profile"
-    select 'bug', from: "Labels"
+    click_button "Label"
+    click_link "bug"
     click_button "Submit issue"
   end
 
@@ -297,7 +300,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
   end
 
   step 'I fill in issue search with \'Rock and roll\'' do
-    filter_issue 'Description for issue'
+    filter_issue 'Rock and roll'
   end
 
   step 'I should see \'Bugfix1\' in issues' do
@@ -354,8 +357,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
   end
 
   def filter_issue(text)
-    sleep 1
-    fill_in 'issue_search', with: text
-    sleep 1
+    fill_in 'issuable_search', with: text
   end
 end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index 2937d5d7ca8a6647a7235feaff8c11d28fba6db2..f74a9b5df47c19898eb8d93e72a5e0d53bacdbff 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
   end
 
   step 'I remove label \'bug\'' do
-    page.within "#label_#{bug_label.id}" do
+    page.within "#project_label_#{bug_label.id}" do
       first(:link, 'Delete').click
     end
   end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 53d1aedf27f22e2305995564db83cb8b332bed2c..2ccab4334eb6797e12d52dd3a733858207c9f16b 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -7,6 +7,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
   include SharedMarkdown
   include SharedDiffNote
   include SharedUser
+  include WaitForAjax
+
+  after do
+    wait_for_ajax if javascript_test?
+  end
 
   step 'I click link "New Merge Request"' do
     click_link "New Merge Request"
@@ -22,6 +27,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
 
   step 'I click link "All"' do
     click_link "All"
+    # Waits for load
+    expect(find('.issues-state-filters > .active')).to have_content 'All'
   end
 
   step 'I click link "Merged"' do
@@ -29,7 +36,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
   end
 
   step 'I click link "Closed"' do
-    click_link "Closed"
+    page.within('.issues-state-filters') do
+      click_link "Closed"
+    end
   end
 
   step 'I should see merge request "Wiki Feature"' do
@@ -56,8 +65,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
     expect(find('.merge-request-info')).not_to have_content "master"
   end
 
-  step 'I should see "other_branch" branch' do
-    expect(page).to have_content "other_branch"
+  step 'I should see "feature_conflict" branch' do
+    expect(page).to have_content "feature_conflict"
   end
 
   step 'I should see "Bug NS-04" in merge requests' do
@@ -86,6 +95,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
 
   step 'I click button "Unsubscribe"' do
     click_on "Unsubscribe"
+    wait_for_ajax
   end
 
   step 'I click link "Close"' do
@@ -110,7 +120,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
            source_project: project,
            target_project: project,
            source_branch: 'fix',
-           target_branch: 'master',
+           target_branch: 'merge-test',
            author: project.users.first,
            description: "# Description header"
           )
@@ -122,7 +132,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
            source_project: project,
            target_project: project,
            source_branch: 'fix',
-           target_branch: 'other_branch',
+           target_branch: 'feature_conflict',
            author: project.users.first,
            description: "# Description header"
           )
@@ -133,7 +143,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
            title: "Bug NS-05",
            source_project: project,
            target_project: project,
-           author: project.users.first)
+           author: project.users.first,
+           source_branch: 'merge-test')
   end
 
   step 'project "Shop" have "Feature NS-05" merged merge request' do
@@ -489,8 +500,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
   end
 
   step 'I fill in merge request search with "Fe"' do
-    sleep 1
-    fill_in 'issue_search', with: "Fe"
+    fill_in 'issuable_search', with: "Fe"
   end
 
   step 'I click the "Target branch" dropdown' do
@@ -505,7 +515,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
 
   step 'I should see new target branch changes' do
     expect(page).to have_content 'Request to merge fix into feature'
-    expect(page).to have_content 'Target branch changed from master to feature'
+    expect(page).to have_content 'Target branch changed from merge-test to feature'
+    wait_for_ajax
   end
 
   step 'I click on "Email Patches"' do
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 019b3124a863d5b5d6de957750e01dd69c9dc5b3..ff9251615c930d84c146e83e15cf879c132bcf53 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -109,4 +109,8 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
       find('button').click
     end
   end
+
+  step 'I should see non-existent git revision error message' do
+    expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist."
+  end
 end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 76fefee9254f12cae6041e1aba43654b12f143d4..975c879149e5dae8531480f18acf6191bd7e69b6 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
 
   step 'change project settings' do
     fill_in 'project_name_edit', with: 'NewName'
-    uncheck 'project_issues_enabled'
+    select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
   end
 
   step 'I save project' do
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 536199ddb4fd7c25f766b7a93b5a74dd992490e0..bd6466f3686af1dd75ed45ed7a5318ed4e526f93 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -178,17 +178,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
   end
 
   step 'I fill jira settings' do
-    fill_in 'Project url', with: 'http://jira.example'
+    fill_in 'URL', with: 'http://jira.example'
     fill_in 'Username', with: 'gitlab'
     fill_in 'Password', with: 'gitlab'
-    fill_in 'Api url', with: 'http://jira.example/rest/api/2'
+    fill_in 'Project Key', with: 'GITLAB'
     click_button 'Save'
   end
 
   step 'I should see jira service settings saved' do
-    expect(find_field('Project url').value).to eq 'http://jira.example'
+    expect(find_field('URL').value).to eq 'http://jira.example'
     expect(find_field('Username').value).to eq 'gitlab'
-    expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2'
+    expect(find_field('Project Key').value).to eq 'GITLAB'
   end
 
   step 'I click Atlassian Bamboo CI service link' do
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index beb8ecfc799254150314886eda3110190a55ab59..5e7d539add6c181be89681d7597eb198297d4fe1 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -21,8 +21,8 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
            author: project.users.first)
   end
 
-  step 'I click link "New Snippet"' do
-    click_link "New Snippet"
+  step 'I click link "New snippet"' do
+    click_link "New snippet"
   end
 
   step 'I click link "Snippet one"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 841d191d55b77244ad7c357eb22b643eef47206a..1cc9e37b0750179d83b3cc1bb85301aacefe9c96 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I should see its content with new lines preserved at end of file' do
-    expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+    expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
   end
 
   step 'I click link "Raw"' do
@@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
 
   step 'I can edit code' do
     set_new_content
-    expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
+    expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
   end
 
   step 'I edit code' do
@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I edit code with new lines at end of file' do
-    execute_script('blob.editor.setValue("Sample\n\n\n")')
+    execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
   end
 
   step 'I fill the new file name' do
@@ -105,6 +105,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
     click_button 'Commit Changes'
   end
 
+  step 'I click on "Changes" tab' do
+    click_link 'Changes'
+  end
+
   step 'I click on "Create directory"' do
     click_button 'Create directory'
   end
@@ -378,7 +382,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   private
 
   def set_new_content
-    execute_script("blob.editor.setValue('#{new_gitignore_content}')")
+    execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
   end
 
   # Content of the gitignore file on the seed repository.
diff --git a/features/steps/project/source/git_blame.rb b/features/steps/project/source/git_blame.rb
deleted file mode 100644
index d0a27f47e2a4739cae9ed33c5f157d2246e779bc..0000000000000000000000000000000000000000
--- a/features/steps/project/source/git_blame.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Spinach::Features::ProjectSourceGitBlame < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedProject
-  include SharedPaths
-
-  step 'I click on ".gitignore" file in repo' do
-    click_link ".gitignore"
-  end
-
-  step 'I click Blame button' do
-    click_link 'Blame'
-  end
-
-  step 'I should see git file blame' do
-    expect(page).to have_content "*.rb"
-    expect(page).to have_content "Dmitriy Zaporozhets"
-    expect(page).to have_content "Initial commit"
-  end
-end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1b6dcbd33acb87adc56eebf0c5ae44..b21d0849ad1fc2a8954bafb18d6584850b607a72 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
       select2(user.id, from: "#user_ids", multiple: true)
       select "Reporter", from: "access_level"
     end
-    click_button "Add users to project"
+    click_button "Add to project"
   end
 
   step 'I should see "Mike" in team list as "Reporter"' do
@@ -36,10 +36,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
 
   step 'I select "sjobs@apple.com" as "Reporter"' do
     page.within ".users-project-form" do
-      select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+      find('#user_ids', visible: false).set('sjobs@apple.com')
       select "Reporter", from: "access_level"
     end
-    click_button "Add users to project"
+    click_button "Add to project"
   end
 
   step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
@@ -65,9 +65,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
     user = User.find_by(name: 'Dmitriy')
     project_member = project.project_members.find_by(user_id: user.id)
     page.within "#project_member_#{project_member.id}" do
-      click_button "Edit access level"
-      select "Reporter", from: "project_member_access_level"
-      click_button "Save"
+      select "Reporter", from: "member_access_level_#{project_member.id}"
     end
   end
 
@@ -112,7 +110,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   end
 
   step 'I click link "Import team from another project"' do
-    click_link "Import members from another project"
+    click_link "Import"
   end
 
   When 'I submit "Website" project for import team' do
@@ -144,8 +142,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   end
 
   step 'I should see "Opensource" group user listing' do
-    expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
-    expect(page).to have_content(@os_user1.name)
-    expect(page).to have_content(@os_user2.name)
+    page.within '.project-members-groups' do
+      expect(page).to have_content('OpenSource')
+      expect(find('select').value).to eq('40')
+    end
   end
 end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 4df4e89f5b98a33c1bfd7caf347b6908f9b21b74..35b7159970823abaf2236f9120a3c6902f533a14 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -210,7 +210,7 @@ module SharedDiffNote
   end
 
   step 'I click side-by-side diff button' do
-    find('#parallel-diff-btn').click
+    find('#parallel-diff-btn').trigger('click')
   end
 
   step 'I see side-by-side diff button' do
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index aa666a954bca40aed5e359a1b44c8b9c3bee039c..df9845ba569dac0788ce79c6e311246af309a87e 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -179,7 +179,7 @@ module SharedIssuable
     project = Project.find_by(name: from_project_name)
 
     expect(page).to have_content(user_name)
-    expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+    expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
   end
 
   def expect_sidebar_content(content)
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 3d7c6ef9d2db0636078f63e2c04696eac4027585..9dc1fc41b3b9b00f875de462fc9a63fbc5ffc954 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -1,5 +1,6 @@
 module SharedNote
   include Spinach::DSL
+  include WaitForAjax
 
   step 'I delete a comment' do
     page.within('.main-notes-list') do
@@ -116,8 +117,9 @@ module SharedNote
     page.within(".js-main-target-form") do
       fill_in "note[note]", with: "# Comment with a header"
       click_button "Comment"
-      sleep 0.05
     end
+
+    wait_for_ajax
   end
 
   step 'The comment with the header should not have an ID' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 0b4920883b86248c601477d5acae6f85b34f5fe2..cab85a48396b69465c2e0b2013ff532836529360 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -15,7 +15,7 @@ module SharedProject
   # Create a specific project called "Shop"
   step 'I own project "Shop"' do
     @project = Project.find_by(name: "Shop")
-    @project ||= create(:project, name: "Shop", namespace: @user.namespace, snippets_enabled: true)
+    @project ||= create(:project, name: "Shop", namespace: @user.namespace)
     @project.team << [@user, :master]
   end
 
@@ -41,6 +41,8 @@ module SharedProject
   step 'I own project "Forum"' do
     @project = Project.find_by(name: "Forum")
     @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+    @project.build_project_feature
+    @project.project_feature.save
     @project.team << [@user, :master]
   end
 
@@ -95,7 +97,7 @@ module SharedProject
   step 'I should see project settings' do
     expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
     expect(page).to have_content("Project name")
-    expect(page).to have_content("Features")
+    expect(page).to have_content("Feature Visibility")
   end
 
   def current_project
@@ -216,6 +218,10 @@ module SharedProject
     2.times { create(:note_on_issue, project: project) }
   end
 
+  step 'trending projects are refreshed' do
+    TrendingProject.refresh!
+  end
+
   step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
     project = Project.find_by(name: "Shop")
     create(:label, project: project, title: 'bug')
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
index 5c47238777fd77113aa51d2d7b150185afe19ed0..07fff16e867fe0f985b46e5629e6a10d1c3ff6a8 100644
--- a/features/steps/shared/sidebar_active_tab.rb
+++ b/features/steps/shared/sidebar_active_tab.rb
@@ -1,12 +1,8 @@
 module SharedSidebarActiveTab
   include Spinach::DSL
 
-  step 'the active main tab should be Help' do
-    ensure_active_main_tab('Help')
-  end
-
   step 'no other main tabs should be active' do
-    expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+    expect(page).to have_selector('.nav-sidebar li.active', count: 1)
   end
 
   def ensure_active_main_tab(content)
@@ -17,6 +13,10 @@ module SharedSidebarActiveTab
     ensure_active_main_tab('Projects')
   end
 
+  step 'the active main tab should be Groups' do
+    ensure_active_main_tab('Groups')
+  end
+
   step 'the active main tab should be Projects' do
     ensure_active_main_tab('Projects')
   end
@@ -28,8 +28,4 @@ module SharedSidebarActiveTab
   step 'the active main tab should be Merge Requests' do
     ensure_active_main_tab('Merge Requests')
   end
-
-  step 'the active main tab should be Help' do
-    ensure_active_main_tab('Help')
-  end
 end
diff --git a/features/steps/snippets/public_snippets.rb b/features/steps/snippets/public_snippets.rb
deleted file mode 100644
index 2ebdca5ed3084e2d4a25e74cf90ff66bc5860f62..0000000000000000000000000000000000000000
--- a/features/steps/snippets/public_snippets.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::PublicSnippets < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedPaths
-  include SharedSnippet
-
-  step 'I should see snippet "Personal snippet one"' do
-    expect(page).to have_no_xpath("//i[@class='public-snippet']")
-  end
-
-  step 'I should see raw snippet "Personal snippet one"' do
-    expect(page).to have_text(snippet.content)
-  end
-
-  step 'I visit snippet page "Personal snippet one"' do
-    visit snippet_path(snippet)
-  end
-
-  step 'I visit snippet raw page "Personal snippet one"' do
-    visit raw_snippet_path(snippet)
-  end
-
-  def snippet
-    @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
-  end
-end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index fe9e39cf50946207870e775d369de4711b538c1b..dae0d0f918cd53e16a467c970969b55de22a2c99 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -20,5 +20,5 @@ unless ENV['CI'] || ENV['CI_SERVER']
 end
 
 Spinach.hooks.before_run do
-  TestEnv.warm_asset_cache
+  TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
 end
diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb
index 1ab308cfa556f4c640c5b4a51b00ae6d5a571aed..8294bb1445f104b2c537187a33aad7811f4002aa 100644
--- a/features/support/db_cleaner.rb
+++ b/features/support/db_cleaner.rb
@@ -1,6 +1,6 @@
 require 'database_cleaner'
 
-DatabaseCleaner.strategy = :truncation
+DatabaseCleaner[:active_record].strategy = :truncation
 
 Spinach.hooks.before_scenario do
   DatabaseCleaner.start
diff --git a/features/support/env.rb b/features/support/env.rb
index 569fd444e8652858d763562332b03354a27fb10f..8dbe3624410841dfa79af5589444d19be2de1d8d 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -15,7 +15,7 @@ if ENV['CI']
   Knapsack::Adapters::SpinachAdapter.bind
 end
 
-%w(select2_helper test_env repo_helpers).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_ajax).each do |f|
   require Rails.root.join('spec', 'support', f)
 end
 
diff --git a/features/support/rerun.rb b/features/support/rerun.rb
index 8b176c5be895e397ddacb4c085488c9dee9ef5bc..60b78f9d05079d92df9d6e652f76349bc15b4f50 100644
--- a/features/support/rerun.rb
+++ b/features/support/rerun.rb
@@ -1,5 +1,7 @@
 # The spinach-rerun-reporter doesn't define the on_undefined_step
 # See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
+require 'spinach-rerun-reporter'
+
 module Spinach
   class Reporter
     class Rerun
diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb
deleted file mode 100644
index b90fc1126716d1511e5af745ef3e28026ce3bef6..0000000000000000000000000000000000000000
--- a/features/support/wait_for_ajax.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module WaitForAjax
-  def wait_for_ajax
-    Timeout.timeout(Capybara.default_max_wait_time) do
-      loop until finished_all_ajax_requests?
-    end
-  end
-
-  def finished_all_ajax_requests?
-    page.evaluate_script('jQuery.active').zero?
-  end
-end
diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a7b8d5bf3510616d80db6a5d473d81838b16e1c
--- /dev/null
+++ b/generator_templates/rails/post_deployment_migration/migration.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class <%= migration_class_name %> < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  # 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
+  end
+end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index d02b469dac8bb25014fa3ea7c261e439e94553ac..87915b194807c76235c1e9eb83d89fb53b0d43ec 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -5,32 +5,27 @@ module API
     helpers ::API::Helpers::MembersHelpers
 
     %w[group project].each do |source_type|
+      params do
+        requires :id, type: String, desc: "The #{source_type} ID"
+      end
       resource source_type.pluralize do
-        # Get a list of group/project access requests viewable by the authenticated user.
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #
-        # Example Request:
-        #  GET /groups/:id/access_requests
-        #  GET /projects/:id/access_requests
+        desc "Gets a list of access requests for a #{source_type}." do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::AccessRequester
+        end
         get ":id/access_requests" do
           source = find_source(source_type, params[:id])
-          authorize_admin_source!(source_type, source)
 
-          access_requesters = paginate(source.requesters.includes(:user))
+          access_requesters = AccessRequestsFinder.new(source).execute!(current_user)
+          access_requesters = paginate(access_requesters.includes(:user))
 
-          present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
+          present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
         end
 
-        # Request access to the group/project
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #
-        # Example Request:
-        #  POST /groups/:id/access_requests
-        #  POST /projects/:id/access_requests
+        desc "Requests access for the authenticated user to a #{source_type}." do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::AccessRequester
+        end
         post ":id/access_requests" do
           source = find_source(source_type, params[:id])
           access_requester = source.request_access(current_user)
@@ -42,47 +37,34 @@ module API
           end
         end
 
-        # Approve a group/project access request
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the access requester
-        #   access_level (optional) - Access level
-        #
-        # Example Request:
-        #   PUT /groups/:id/access_requests/:user_id/approve
-        #   PUT /projects/:id/access_requests/:user_id/approve
+        desc 'Approves an access request for the given user.' do
+          detail 'This feature was introduced in GitLab 8.11.'
+          success Entities::Member
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+          optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+        end
         put ':id/access_requests/:user_id/approve' do
-          required_attributes! [:user_id]
           source = find_source(source_type, params[:id])
-          authorize_admin_source!(source_type, source)
 
-          member = source.requesters.find_by!(user_id: params[:user_id])
-          if params[:access_level]
-            member.update(access_level: params[:access_level])
-          end
-          member.accept_request
+          member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
 
           status :created
           present member.user, with: Entities::Member, member: member
         end
 
-        # Deny a group/project access request
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the access requester
-        #
-        # Example Request:
-        #   DELETE /groups/:id/access_requests/:user_id
-        #   DELETE /projects/:id/access_requests/:user_id
+        desc 'Denies an access request for the given user.' do
+          detail 'This feature was introduced in GitLab 8.11.'
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+        end
         delete ":id/access_requests/:user_id" do
-          required_attributes! [:user_id]
           source = find_source(source_type, params[:id])
 
-          access_requester = source.requesters.find_by!(user_id: params[:user_id])
-
-          ::Members::DestroyService.new(access_requester, current_user).execute
+          ::Members::DestroyService.new(source, current_user, params).
+            execute(:requesters)
         end
       end
     end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d43af3f24e9586edea1c1cbe793297cb59f4d40f..67109ceeef98f93638281c66f10344b37edd4f7c 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -18,31 +18,27 @@ module API
     end
 
     rescue_from :all do |exception|
-      # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
-      # why is this not wrapped in something reusable?
-      trace = exception.backtrace
-
-      message = "\n#{exception.class} (#{exception.message}):\n"
-      message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
-      message << "  " << trace.join("\n  ")
-
-      API.logger.add Logger::FATAL, message
-      rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
+      handle_api_exception(exception)
     end
 
     format :json
     content_type :txt, "text/plain"
 
     # Ensure the namespace is right, otherwise we might load Grape::API::Helpers
+    helpers ::SentryHelper
     helpers ::API::Helpers
 
+    # Keep in alphabetical order
     mount ::API::AccessRequests
     mount ::API::AwardEmoji
+    mount ::API::Boards
     mount ::API::Branches
+    mount ::API::BroadcastMessages
     mount ::API::Builds
-    mount ::API::CommitStatuses
     mount ::API::Commits
+    mount ::API::CommitStatuses
     mount ::API::DeployKeys
+    mount ::API::Deployments
     mount ::API::Environments
     mount ::API::Files
     mount ::API::Groups
@@ -50,15 +46,18 @@ module API
     mount ::API::Issues
     mount ::API::Keys
     mount ::API::Labels
-    mount ::API::LicenseTemplates
+    mount ::API::Lint
     mount ::API::Members
+    mount ::API::MergeRequestDiffs
     mount ::API::MergeRequests
     mount ::API::Milestones
     mount ::API::Namespaces
     mount ::API::Notes
+    mount ::API::NotificationSettings
+    mount ::API::Pipelines
     mount ::API::ProjectHooks
-    mount ::API::ProjectSnippets
     mount ::API::Projects
+    mount ::API::ProjectSnippets
     mount ::API::Repositories
     mount ::API::Runners
     mount ::API::Services
@@ -73,5 +72,10 @@ module API
     mount ::API::Triggers
     mount ::API::Users
     mount ::API::Variables
+    mount ::API::Version
+
+    route :any, '*path' do
+      error!('404 Not Found', 404)
+    end
   end
 end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 7e67edb203ac73243a45a4f3e0eed0d583a10e6c..8cc7a26f1fa71249e79d28f71308a5fdc097a251 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -33,46 +33,29 @@ module API
       #
       # If the token is revoked, then it raises RevokedError.
       #
-      # If the token is not found (nil), then it raises TokenNotFoundError.
+      # If the token is not found (nil), then it returns nil
       #
       # Arguments:
       #
       #   scopes: (optional) scopes required for this guard.
       #           Defaults to empty array.
       #
-      def doorkeeper_guard!(scopes: [])
-        if (access_token = find_access_token).nil?
-          raise TokenNotFoundError
-
-        else
-          case validate_access_token(access_token, scopes)
-          when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
-            raise InsufficientScopeError.new(scopes)
-          when Oauth2::AccessTokenValidationService::EXPIRED
-            raise ExpiredError
-          when Oauth2::AccessTokenValidationService::REVOKED
-            raise RevokedError
-          when Oauth2::AccessTokenValidationService::VALID
-            @current_user = User.find(access_token.resource_owner_id)
-          end
-        end
-      end
-
       def doorkeeper_guard(scopes: [])
-        if access_token = find_access_token
-          case validate_access_token(access_token, scopes)
-          when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
-            raise InsufficientScopeError.new(scopes)
+        access_token = find_access_token
+        return nil unless access_token
+
+        case validate_access_token(access_token, scopes)
+        when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+          raise InsufficientScopeError.new(scopes)
 
-          when Oauth2::AccessTokenValidationService::EXPIRED
-            raise ExpiredError
+        when Oauth2::AccessTokenValidationService::EXPIRED
+          raise ExpiredError
 
-          when Oauth2::AccessTokenValidationService::REVOKED
-            raise RevokedError
+        when Oauth2::AccessTokenValidationService::REVOKED
+          raise RevokedError
 
-          when Oauth2::AccessTokenValidationService::VALID
-            @current_user = User.find(access_token.resource_owner_id)
-          end
+        when Oauth2::AccessTokenValidationService::VALID
+          @current_user = User.find(access_token.resource_owner_id)
         end
       end
 
@@ -96,19 +79,6 @@ module API
     end
 
     module ClassMethods
-      # Installs the doorkeeper guard on the whole Grape API endpoint.
-      #
-      # Arguments:
-      #
-      #   scopes: (optional) scopes required for this guard.
-      #           Defaults to empty array.
-      #
-      def guard_all!(scopes: [])
-        before do
-          guard! scopes: scopes
-        end
-      end
-
       private
 
       def install_error_responders(base)
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2efe7e3adf3474888034daa4a7233148d190afd5..e9ccba3b465c7714b2770bd5cd024c1794e6739c 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,23 +1,26 @@
 module API
   class AwardEmoji < Grape::API
     before { authenticate! }
-    AWARDABLES = [Issue, MergeRequest]
+    AWARDABLES = %w[issue merge_request snippet]
 
     resource :projects do
       AWARDABLES.each do |awardable_type|
-        awardable_string = awardable_type.to_s.underscore.pluralize
-        awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+        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|
-          # Get a list of project +awardable+ award emoji
-          #
-          # Parameters:
-          #   id (required)           - The ID of a project
-          #   awardable_id (required) - The ID of an issue or MR
-          # Example Request:
-          #   GET /projects/:id/issues/:awardable_id/award_emoji
+
+          desc 'Get a list of project +awardable+ award emoji' do
+            detail 'This feature was introduced in 8.9'
+            success Entities::AwardEmoji
+          end
           get endpoint do
             if can_read_awardable?
               awards = paginate(awardable.award_emoji)
@@ -27,14 +30,13 @@ module API
             end
           end
 
-          # Get a specific award emoji
-          #
-          # Parameters:
-          #   id (required)           - The ID of a project
-          #   awardable_id (required) - The ID of an issue or MR
-          #   award_id (required)     - The ID of the award
-          # Example Request:
-          #   GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
+          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
@@ -43,18 +45,15 @@ module API
             end
           end
 
-          # Award a new Emoji
-          #
-          # Parameters:
-          #   id (required) - The ID of a project
-          #   awardable_id (required) - The ID of an issue or mr
-          #   name (required) - The name of a award_emoji (without colons)
-          # Example Request:
-          #   POST /projects/:id/issues/:awardable_id/award_emoji
+          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
-            required_attributes! [:name]
-
-            not_found!('Award Emoji') unless can_read_awardable?
+            not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
 
             award = awardable.create_award_emoji(params[:name], current_user)
 
@@ -65,14 +64,13 @@ module API
             end
           end
 
-          # Delete a +awardables+ award emoji
-          #
-          # Parameters:
-          #   id (required) - The ID of a project
-          #   awardable_id (required) - The ID of an issue or MR
-          #   award_emoji_id (required) - The ID of an award emoji
-          # Example Request:
-          #   DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+          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])
 
@@ -87,27 +85,36 @@ module API
 
     helpers do
       def can_read_awardable?
-        ability = "read_#{awardable.class.to_s.underscore}".to_sym
+        can?(current_user, read_ability(awardable), awardable)
+      end
 
-        can?(current_user, ability, awardable)
+      def can_award_awardable?
+        awardable.user_can_award?(current_user, params[:name])
       end
 
       def awardable
         @awardable ||=
           begin
             if params.include?(:note_id)
-              noteable.notes.find(params[: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
-              noteable
+              user_project.snippets.find(params[:snippet_id])
             end
           end
       end
 
-      def noteable
-        if params.include?(:issue_id)
-          user_project.issues.find(params[:issue_id])
+      def read_ability(awardable)
+        case awardable
+        when Note
+          read_ability(awardable.noteable)
         else
-          user_project.merge_requests.find(params[:merge_request_id])
+          :"read_#{awardable.class.to_s.underscore}"
         end
       end
     end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4ac491edc1bf40cc63121dd23875a92b65d9fd50
--- /dev/null
+++ b/lib/api/boards.rb
@@ -0,0 +1,132 @@
+module API
+  # Boards API
+  class Boards < Grape::API
+    before { authenticate! }
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects do
+      desc 'Get all project boards' do
+        detail 'This feature was introduced in 8.13'
+        success Entities::Board
+      end
+      get ':id/boards' do
+        authorize!(:read_board, user_project)
+        present user_project.boards, with: Entities::Board
+      end
+
+      params do
+        requires :board_id, type: Integer, desc: 'The ID of a board'
+      end
+      segment ':id/boards/:board_id' do
+        helpers do
+          def project_board
+            board = user_project.boards.first
+
+            if params[:board_id] == board.id
+              board
+            else
+              not_found!('Board')
+            end
+          end
+
+          def board_lists
+            project_board.lists.destroyable
+          end
+        end
+
+        desc 'Get the lists of a project board' do
+          detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+          success Entities::List
+        end
+        get '/lists' do
+          authorize!(:read_board, user_project)
+          present board_lists, with: Entities::List
+        end
+
+        desc 'Get a list of a project board' do
+          detail 'This feature was introduced in 8.13'
+          success Entities::List
+        end
+        params do
+          requires :list_id, type: Integer, desc: 'The ID of a list'
+        end
+        get '/lists/:list_id' do
+          authorize!(:read_board, user_project)
+          present board_lists.find(params[:list_id]), with: Entities::List
+        end
+
+        desc 'Create a new board list' do
+          detail 'This feature was introduced in 8.13'
+          success Entities::List
+        end
+        params do
+          requires :label_id, type: Integer, desc: 'The ID of an existing label'
+        end
+        post '/lists' do
+          unless available_labels.exists?(params[:label_id])
+            render_api_error!({ error: 'Label not found!' }, 400)
+          end
+
+          authorize!(:admin_list, user_project)
+
+          service = ::Boards::Lists::CreateService.new(user_project, current_user,
+            { label_id: params[:label_id] })
+
+          list = service.execute(project_board)
+
+          if list.valid?
+            present list, with: Entities::List
+          else
+            render_validation_error!(list)
+          end
+        end
+
+        desc 'Moves a board list to a new position' do
+          detail 'This feature was introduced in 8.13'
+          success Entities::List
+        end
+        params do
+          requires :list_id,  type: Integer, desc: 'The ID of a list'
+          requires :position, type: Integer, desc: 'The position of the list'
+        end
+        put '/lists/:list_id' do
+          list = project_board.lists.movable.find(params[:list_id])
+
+          authorize!(:admin_list, user_project)
+
+          service = ::Boards::Lists::MoveService.new(user_project, current_user,
+              { position: params[:position] })
+
+          if service.execute(list)
+            present list, with: Entities::List
+          else
+            render_api_error!({ error: "List could not be moved!" }, 400)
+          end
+        end
+
+        desc 'Delete a board list' do
+          detail 'This feature was introduced in 8.13'
+          success 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: Entities::List
+          else
+            render_api_error!({ error: 'List could not be deleted!' }, 400)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index b615703df936c11a2f757dfc2ebbd5253284c7e4..21a106387f083ff748ffaf57b8fc3cb4f8e7b070 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -6,124 +6,100 @@ module API
     before { authenticate! }
     before { authorize! :download_code, user_project }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get a project repository branches
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      # Example Request:
-      #   GET /projects/:id/repository/branches
+      desc 'Get a project repository branches' do
+        success Entities::RepoBranch
+      end
       get ":id/repository/branches" do
         branches = user_project.repository.branches.sort_by(&:name)
 
         present branches, with: Entities::RepoBranch, project: user_project
       end
 
-      # Get a single branch
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   branch (required) - The name of the branch
-      # Example Request:
-      #   GET /projects/:id/repository/branches/:branch
-      get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
-        @branch = user_project.repository.branches.find { |item| item.name == params[:branch] }
-        not_found!("Branch") unless @branch
+      desc 'Get a single branch' do
+        success Entities::RepoBranch
+      end
+      params do
+        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+      end
+      get ':id/repository/branches/:branch' do
+        branch = user_project.repository.find_branch(params[:branch])
+        not_found!("Branch") unless branch
 
-        present @branch, with: Entities::RepoBranch, project: user_project
+        present branch, with: Entities::RepoBranch, project: user_project
       end
 
-      # Protect a single branch
-      #
       # Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}`
       # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
       # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   branch (required) - The name of the branch
-      #   developers_can_push (optional) - Flag if developers can push to that branch
-      #   developers_can_merge (optional) - Flag if developers can merge to that branch
-      # Example Request:
-      #   PUT /projects/:id/repository/branches/:branch/protect
-      put ':id/repository/branches/:branch/protect',
-          requirements: { branch: /.+/ } do
+      desc 'Protect a single branch' do
+        success Entities::RepoBranch
+      end
+      params do
+        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+        optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
+        optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
+      end
+      put ':id/repository/branches/:branch/protect' do
         authorize_admin_project
 
-        @branch = user_project.repository.find_branch(params[:branch])
-        not_found!('Branch') unless @branch
-        protected_branch = user_project.protected_branches.find_by(name: @branch.name)
+        branch = user_project.repository.find_branch(params[:branch])
+        not_found!('Branch') unless branch
 
-        developers_can_merge = to_boolean(params[:developers_can_merge])
-        developers_can_push = to_boolean(params[:developers_can_push])
+        protected_branch = user_project.protected_branches.find_by(name: branch.name)
 
         protected_branch_params = {
-          name: @branch.name
+          name: branch.name,
+          developers_can_push: params[:developers_can_push],
+          developers_can_merge: params[:developers_can_merge]
         }
 
-        # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
-        # merge_access_levels need to be deleted.
-        if developers_can_merge == false
-          protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
-        end
+        service_args = [user_project, current_user, protected_branch_params]
 
-        # If `developers_can_push` is switched off, _all_ `DEVELOPER`
-        # push_access_levels need to be deleted.
-        if developers_can_push == false
-          protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
-        end
+        protected_branch = if protected_branch
+                             ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch)
+                           else
+                             ProtectedBranches::ApiCreateService.new(*service_args).execute
+                           end
 
-        protected_branch_params.merge!(
-          merge_access_levels_attributes: [{
-            access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-          }],
-          push_access_levels_attributes: [{
-            access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
-          }]
-        )
-
-        if protected_branch
-          service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
-          service.execute(protected_branch)
+        if protected_branch.valid?
+          present branch, with: Entities::RepoBranch, project: user_project
         else
-          service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params)
-          service.execute
+          render_api_error!(protected_branch.errors.full_messages, 422)
         end
-
-        present @branch, with: Entities::RepoBranch, project: user_project
       end
 
-      # Unprotect a single branch
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   branch (required) - The name of the branch
-      # Example Request:
-      #   PUT /projects/:id/repository/branches/:branch/unprotect
-      put ':id/repository/branches/:branch/unprotect',
-          requirements: { branch: /.+/ } do
+      desc 'Unprotect a single branch' do
+        success Entities::RepoBranch
+      end
+      params do
+        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+      end
+      put ':id/repository/branches/:branch/unprotect' do
         authorize_admin_project
 
-        @branch = user_project.repository.find_branch(params[:branch])
-        not_found!("Branch") unless @branch
-        protected_branch = user_project.protected_branches.find_by(name: @branch.name)
+        branch = user_project.repository.find_branch(params[:branch])
+        not_found!("Branch") unless branch
+        protected_branch = user_project.protected_branches.find_by(name: branch.name)
         protected_branch.destroy if protected_branch
 
-        present @branch, with: Entities::RepoBranch, project: user_project
+        present branch, with: Entities::RepoBranch, project: user_project
       end
 
-      # Create branch
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   branch_name (required) - The name of the branch
-      #   ref (required) - Create branch from commit sha or existing branch
-      # Example Request:
-      #   POST /projects/:id/repository/branches
+      desc 'Create branch' do
+        success 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])
+                 execute(params[:branch_name], params[:ref])
 
         if result[:status] == :success
           present result[:branch],
@@ -134,18 +110,15 @@ module API
         end
       end
 
-      # Delete branch
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   branch (required) - The name of the branch
-      # Example Request:
-      #   DELETE /projects/:id/repository/branches/:branch
-      delete ":id/repository/branches/:branch",
-          requirements: { branch: /.+/ } do
+      desc 'Delete a branch'
+      params do
+        requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+      end
+      delete ":id/repository/branches/:branch" do
         authorize_push_project
+
         result = DeleteBranchService.new(user_project, current_user).
-          execute(params[:branch])
+                 execute(params[:branch])
 
         if result[:status] == :success
           {
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fb2a41480113d71390b2ca2d5f18a192e541ec4d
--- /dev/null
+++ b/lib/api/broadcast_messages.rb
@@ -0,0 +1,99 @@
+module API
+  class BroadcastMessages < Grape::API
+    before { authenticate! }
+    before { authenticated_as_admin! }
+
+    resource :broadcast_messages do
+      helpers do
+        def find_message
+          BroadcastMessage.find(params[:id])
+        end
+      end
+
+      desc 'Get all broadcast messages' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        optional :page,     type: Integer, desc: 'Current page number'
+        optional :per_page, type: Integer, desc: 'Number of messages per page'
+      end
+      get do
+        messages = BroadcastMessage.all
+
+        present paginate(messages), with: Entities::BroadcastMessage
+      end
+
+      desc 'Create a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :message,   type: String,   desc: 'Message to display'
+        optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now }
+        optional :ends_at,   type: DateTime, desc: 'Ending time',   default: -> { 1.hour.from_now }
+        optional :color,     type: String,   desc: 'Background color'
+        optional :font,      type: String,   desc: 'Foreground color'
+      end
+      post do
+        create_params = declared(params, include_missing: false).to_h
+        message = BroadcastMessage.create(create_params)
+
+        if message.persisted?
+          present message, with: Entities::BroadcastMessage
+        else
+          render_validation_error!(message)
+        end
+      end
+
+      desc 'Get a specific broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id, type: Integer, desc: 'Broadcast message ID'
+      end
+      get ':id' do
+        message = find_message
+
+        present message, with: Entities::BroadcastMessage
+      end
+
+      desc 'Update a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id,        type: Integer,  desc: 'Broadcast message ID'
+        optional :message,   type: String,   desc: 'Message to display'
+        optional :starts_at, type: DateTime, desc: 'Starting time'
+        optional :ends_at,   type: DateTime, desc: 'Ending time'
+        optional :color,     type: String,   desc: 'Background color'
+        optional :font,      type: String,   desc: 'Foreground color'
+      end
+      put ':id' do
+        message = find_message
+        update_params = declared(params, include_missing: false).to_h
+
+        if message.update(update_params)
+          present message, with: Entities::BroadcastMessage
+        else
+          render_validation_error!(message)
+        end
+      end
+
+      desc 'Delete a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id, type: Integer, desc: 'Broadcast message ID'
+      end
+      delete ':id' do
+        message = find_message
+
+        present message.destroy, with: Entities::BroadcastMessage
+      end
+    end
+  end
+end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index be5a3484ec8a8143f8fef3a64fb90a312567863c..67adca6605fc124ad82cbae3c578dd7e421f94f7 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -3,15 +3,32 @@ module API
   class Builds < Grape::API
     before { authenticate! }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get a project builds
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
-      #                      if none provided showing all builds)
-      # Example Request:
-      #   GET /projects/:id/builds
+      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'],
+                           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 Entities::Build
+      end
+      params do
+        use :optional_scope
+      end
       get ':id/builds' do
         builds = user_project.builds.order('id DESC')
         builds = filter_builds(builds, params[:scope])
@@ -20,15 +37,13 @@ module API
                                   user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
 
-      # Get builds for a specific commit of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The SHA id of a commit
-      #   scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
-      #                      if none provided showing all builds)
-      # Example Request:
-      #   GET /projects/:id/repository/commits/:sha/builds
+      desc 'Get builds for a specific commit of a project' do
+        success Entities::Build
+      end
+      params do
+        requires :sha,   type: String, desc: 'The SHA id of a commit'
+        use :optional_scope
+      end
       get ':id/repository/commits/:sha/builds' do
         authorize_read_builds!
 
@@ -42,13 +57,12 @@ module API
                                   user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
 
-      # Get a specific build of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   build_id (required) - The ID of a build
-      # Example Request:
-      #   GET /projects/:id/builds/:build_id
+      desc 'Get a specific build of a project' do
+        success 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!
 
@@ -58,13 +72,12 @@ module API
                        user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
 
-      # Download the artifacts file from build
-      #
-      # Parameters:
-      #   id (required) - The ID of a build
-      #   token (required) - The build authorization token
-      # Example Request:
-      #   GET /projects/:id/builds/:build_id/artifacts
+      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!
 
@@ -73,14 +86,13 @@ module API
         present_artifacts!(build.artifacts_file)
       end
 
-      # Download the artifacts file from ref_name and job
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   ref_name (required) - The ref from repository
-      #   job (required) - The name for the build
-      # Example Request:
-      #   GET /projects/:id/builds/artifacts/:ref_name/download?job=name
+      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!
@@ -91,17 +103,13 @@ module API
         present_artifacts!(latest_build.artifacts_file)
       end
 
-      # Get a trace of a specific build of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   build_id (required) - The ID of a build
-      # Example Request:
-      #   GET /projects/:id/build/:build_id/trace
-      #
       # 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!
 
@@ -115,13 +123,12 @@ module API
         body trace
       end
 
-      # Cancel a specific build of a project
-      #
-      # parameters:
-      #   id (required) - the id of a project
-      #   build_id (required) - the id of a build
-      # example request:
-      #   post /projects/:id/build/:build_id/cancel
+      desc 'Cancel a specific build of a project' do
+        success 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!
 
@@ -133,13 +140,12 @@ module API
                        user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
 
-      # Retry a specific build of a project
-      #
-      # parameters:
-      #   id (required) - the id of a project
-      #   build_id (required) - the id of a build
-      # example request:
-      #   post /projects/:id/build/:build_id/retry
+      desc 'Retry a specific build of a project' do
+        success 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!
 
@@ -152,13 +158,12 @@ module API
                        user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
 
-      # Erase build (remove artifacts and build trace)
-      #
-      # Parameters:
-      #   id (required) - the id of a project
-      #   build_id (required) - the id of a build
-      # example Request:
-      #  post  /projects/:id/build/:build_id/erase
+      desc 'Erase build (remove artifacts and build trace)' do
+        success 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!
 
@@ -170,13 +175,12 @@ module API
                        user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
       end
 
-      # Keep the artifacts to prevent them from being deleted
-      #
-      # Parameters:
-      #   id (required) - the id of a project
-      #   build_id (required) - The ID of a build
-      # Example Request:
-      #   POST /projects/:id/builds/:build_id/artifacts/keep
+      desc 'Keep the artifacts to prevent them from being deleted' do
+        success 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!
 
@@ -189,6 +193,27 @@ module API
         present build, with: Entities::Build,
                        user_can_download_artifacts: can?(current_user, :read_build, user_project)
       end
+
+      desc 'Trigger a manual build' do
+        success 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 Build") 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)
+      end
     end
 
     helpers do
@@ -214,14 +239,6 @@ module API
         return builds if scope.nil? || scope.empty?
 
         available_statuses = ::CommitStatus::AVAILABLE_STATUSES
-        scope =
-          if scope.is_a?(String)
-            [scope]
-          elsif scope.is_a?(Hashie::Mash)
-            scope.values
-          else
-            ['unknown']
-          end
 
         unknown = scope - available_statuses
         render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4df6ca8333ecc10cf05166dd9263e05b8075f4b8..f54d4f06627ea96e3946142f7ceabf0d79862663 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -6,17 +6,17 @@ module API
     resource :projects do
       before { authenticate! }
 
-      # Get a commit's statuses
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit hash
-      #   ref (optional) - The ref
-      #   stage (optional) - The stage
-      #   name (optional) - The name
-      #   all (optional) - Show all statuses, default: false
-      # Examples:
-      #   GET /projects/:id/repository/commits/:sha/statuses
+      desc "Get a commit's statuses" do
+        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'
+        optional :name,  type: String, desc: 'The name'
+        optional :all,   type: String, desc: 'Show all statuses, default: false'
+      end
       get ':id/repository/commits/:sha/statuses' do
         authorize!(:read_commit_status, user_project)
 
@@ -31,22 +31,23 @@ module API
         present paginate(statuses), with: Entities::CommitStatus
       end
 
-      # Post status to commit
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit hash
-      #   ref (optional) - The ref
-      #   state (required) - The state of the status. Can be: pending, running, success, error or failure
-      #   target_url (optional) - The target URL to associate with this status
-      #   description (optional) - A short description of the status
-      #   name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default"
-      # Examples:
-      #   POST /projects/:id/statuses/:sha
+      desc 'Post status to a commit' do
+        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']
+        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'
+        optional :name,        type: String,  desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+        optional :context,     type: String,  desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+      end
       post ':id/statuses/:sha' do
         authorize! :create_commit_status, user_project
-        required_attributes! [:state]
-        attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name]
+
         commit = @project.commit(params[:sha])
         not_found! 'Commit' unless commit
 
@@ -58,36 +59,43 @@ module API
         # the first found branch on that commit
 
         ref = params[:ref]
-        unless ref
-          branches = @project.repository.branch_names_contains(commit.sha)
-          not_found! 'References for commit' if branches.none?
-          ref = branches.first
-        end
+        ref ||= @project.repository.branch_names_contains(commit.sha).first
+        not_found! 'References for commit' unless ref
 
-        pipeline = @project.ensure_pipeline(commit.sha, ref, current_user)
+        name = params[:name] || params[:context] || 'default'
 
-        name = params[:name] || params[:context]
-        status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
-        status ||= GenericCommitStatus.new(project: @project, pipeline: pipeline, user: current_user)
-        status.update(attrs)
+        pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
 
-        case params[:state].to_s
-        when 'running'
-          status.run
-        when 'success'
-          status.success
-        when 'failed'
-          status.drop
-        when 'canceled'
-          status.cancel
-        else
-          status.status = params[:state].to_s
-        end
+        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]
+        )
+
+        begin
+          case params[:state].to_s
+          when 'pending'
+            status.enqueue!
+          when 'running'
+            status.enqueue
+            status.run!
+          when 'success'
+            status.success!
+          when 'failed'
+            status.drop!
+          when 'canceled'
+            status.cancel!
+          else
+            render_api_error!('invalid state', 400)
+          end
 
-        if status.save
           present status, with: Entities::CommitStatus
-        else
-          render_validation_error!(status)
+        rescue StateMachines::InvalidTransition => e
+          render_api_error!(e.message, 400)
         end
       end
     end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index b4eaf1813d4958c4fafb7ddb2fe7a390d3f481f7..2f2cf7694817e313ef6dc4675a6ee2e0d3a00f4c 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -6,102 +6,149 @@ module API
     before { authenticate! }
     before { authorize! :download_code, user_project }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get a project repository commits
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
-      #   since (optional) - Only commits after or in this date will be returned
-      #   until (optional) - Only commits before or in this date will be returned
-      # Example Request:
-      #   GET /projects/:id/repository/commits
+      desc 'Get a project repository commits' do
+        success Entities::RepoCommit
+      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 :since,    type: String, desc: 'Only commits after or in this date will be returned'
+        optional :until,    type: String, desc: 'Only commits before or in 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'
+      end
       get ":id/repository/commits" do
+        # TODO remove the next line for 9.0, use DateTime type in the params block
         datetime_attributes! :since, :until
 
-        page = (params[:page] || 0).to_i
-        per_page = (params[:per_page] || 20).to_i
         ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
-        after = params[:since]
-        before = params[:until]
+        offset = params[:page] * params[:per_page]
+
+        commits = user_project.repository.commits(ref,
+                                                  path: params[:path],
+                                                  limit: params[:per_page],
+                                                  offset: offset,
+                                                  after: params[:since],
+                                                  before: params[:until])
 
-        commits = user_project.repository.commits(ref, limit: per_page, offset: page * per_page, after: after, before: before)
         present commits, with: Entities::RepoCommit
       end
 
-      # Get a specific commit of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit hash or name of a repository branch or tag
-      # Example Request:
-      #   GET /projects/:id/repository/commits/:sha
+      desc 'Commit multiple file changes as one commit' do
+        success Entities::RepoCommitDetail
+        detail 'This feature was introduced in GitLab 8.13'
+      end
+      params do
+        requires :id, type: Integer, desc: 'The project ID'
+        requires :branch_name, type: String, desc: 'The name of branch'
+        requires :commit_message, type: String, desc: 'Commit message'
+        requires :actions, type: Array, desc: 'Actions to perform in commit'
+        optional :author_email, type: String, desc: 'Author email for commit'
+        optional :author_name, type: String, desc: 'Author name for commit'
+      end
+      post ":id/repository/commits" do
+        authorize! :push_code, user_project
+
+        attrs = declared(params)
+        attrs[:source_branch] = attrs[:branch_name]
+        attrs[:target_branch] = attrs[:branch_name]
+        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
+          commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+          present commit_detail, with: Entities::RepoCommitDetail
+        else
+          render_api_error!(result[:message], 400)
+        end
+      end
+
+      desc 'Get a specific commit of a project' do
+        success Entities::RepoCommitDetail
+        failure [[404, 'Not Found']]
+      end
+      params do
+        requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+      end
       get ":id/repository/commits/:sha" do
-        sha = params[:sha]
-        commit = user_project.commit(sha)
+        commit = user_project.commit(params[:sha])
+
         not_found! "Commit" unless commit
+
         present commit, with: Entities::RepoCommitDetail
       end
 
-      # Get the diff for a specific commit of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit or branch name
-      # Example Request:
-      #   GET /projects/:id/repository/commits/:sha/diff
+      desc 'Get the diff for a specific commit of a project' do
+        failure [[404, 'Not Found']]
+      end
+      params do
+        requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+      end
       get ":id/repository/commits/:sha/diff" do
-        sha = params[:sha]
-        commit = user_project.commit(sha)
+        commit = user_project.commit(params[:sha])
+
         not_found! "Commit" unless commit
+
         commit.raw_diffs.to_a
       end
 
-      # Get a commit's comments
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit hash
-      # Examples:
-      #   GET /projects/:id/repository/commits/:sha/comments
+      desc "Get a commit's comments" do
+        success Entities::CommitNote
+        failure [[404, 'Not Found']]
+      end
+      params do
+        requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+        optional :per_page, type: Integer, desc: 'The amount of items per page for paginaion'
+        optional :page, type: Integer, desc: 'The page number for pagination'
+      end
       get ':id/repository/commits/:sha/comments' do
-        sha = params[:sha]
-        commit = user_project.commit(sha)
+        commit = user_project.commit(params[:sha])
+
         not_found! 'Commit' unless commit
         notes = Note.where(commit_id: commit.id).order(:created_at)
+
         present paginate(notes), with: Entities::CommitNote
       end
 
-      # Post comment to commit
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   sha (required) - The commit hash
-      #   note (required) - Text of comment
-      #   path (optional) - The file path
-      #   line (optional) - The line number
-      #   line_type (optional) - The type of line (new or old)
-      # Examples:
-      #   POST /projects/:id/repository/commits/:sha/comments
+      desc 'Post comment to commit' do
+        success Entities::CommitNote
+      end
+      params do
+        requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
+        requires :note, type: String, desc: 'The text of the comment'
+        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'
+        end
+      end
       post ':id/repository/commits/:sha/comments' do
-        required_attributes! [:note]
-
-        sha = params[:sha]
-        commit = user_project.commit(sha)
+        commit = user_project.commit(params[:sha])
         not_found! 'Commit' unless commit
+
         opts = {
           note: params[:note],
           noteable_type: 'Commit',
           commit_id: commit.id
         }
 
-        if params[:path] && params[:line] && params[:line_type]
+        if params[:path]
           commit.raw_diffs(all_diffs: true).each do |diff|
             next unless diff.new_path == params[:path]
             lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
 
             lines.each do |line|
-              next unless line.new_pos == params[:line].to_i && line.type == params[:line_type]
+              next unless line.new_pos == params[:line] && line.type == params[:line_type]
               break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
             end
 
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 825e05fbae3d72c930b3bff691d2339b796c4c4f..425df2c176a2ae996803628fa08660315a4a57af 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -49,18 +49,23 @@ module API
           attrs = attributes_for_keys [:title, :key]
           attrs[:key].strip! if attrs[:key]
 
+          # Check for an existing key joined to this project
           key = user_project.deploy_keys.find_by(key: attrs[:key])
-          present key, with: Entities::SSHKey if key
+          if key
+            present key, with: Entities::SSHKey
+            break
+          end
 
           # Check for available deploy keys in other projects
           key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
           if key
             user_project.deploy_keys << key
             present key, with: Entities::SSHKey
+            break
           end
 
+          # Create a new deploy key
           key = DeployKey.new attrs
-
           if key.valid? && user_project.deploy_keys << key
             present key, with: Entities::SSHKey
           else
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f782bcaf7e91cadcd03947271cdaa9c0af8cb154
--- /dev/null
+++ b/lib/api/deployments.rb
@@ -0,0 +1,40 @@
+module API
+  # Deployments RESTfull API endpoints
+  class Deployments < Grape::API
+    before { authenticate! }
+
+    params do
+      requires :id, type: String, desc: 'The project ID'
+    end
+    resource :projects do
+      desc 'Get all deployments of the project' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Deployment
+      end
+      params do
+        optional :page,     type: Integer, desc: 'Page number of the current request'
+        optional :per_page, type: Integer, desc: 'Number of items per page'
+      end
+      get ':id/deployments' do
+        authorize! :read_deployment, user_project
+
+        present paginate(user_project.deployments), with: Entities::Deployment
+      end
+
+      desc 'Gets a specific deployment' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Deployment
+      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: Entities::Deployment
+      end
+    end
+  end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5df65a2327d4221f53777db41b9073661b4ccd63..147aaf06b1850aed45ecffcbff1aaa297920d263 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -15,7 +15,7 @@ module API
     class User < UserBasic
       expose :created_at
       expose :is_admin?, as: :is_admin
-      expose :bio, :location, :skype, :linkedin, :twitter, :website_url
+      expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
     end
 
     class Identity < Grape::Entity
@@ -43,14 +43,13 @@ module API
     end
 
     class Hook < Grape::Entity
-      expose :id, :url, :created_at
+      expose :id, :url, :created_at, :push_events, :tag_push_events
+      expose :enable_ssl_verification
     end
 
     class ProjectHook < Hook
-      expose :project_id, :push_events
-      expose :issues_events, :merge_requests_events, :tag_push_events
-      expose :note_events, :build_events, :pipeline_events
-      expose :enable_ssl_verification
+      expose :project_id, :issues_events, :merge_requests_events
+      expose :note_events, :build_events, :pipeline_events, :wiki_page_events
     end
 
     class BasicProjectDetails < Grape::Entity
@@ -76,40 +75,58 @@ module API
       expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
       expose :name, :name_with_namespace
       expose :path, :path_with_namespace
-      expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled
+      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[:user]) }
+      expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
+      expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
+      expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
+      expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
+
       expose :created_at, :last_activity_at
       expose :shared_runners_enabled
+      expose :lfs_enabled?, as: :lfs_enabled
       expose :creator_id
       expose :namespace
       expose :forked_from_project, using: 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.issues_enabled? && project.default_issues_tracker? }
+      expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[: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|
         SharedGroup.represent(project.project_group_links.all, options)
       end
+      expose :only_allow_merge_if_build_succeeds
+      expose :request_access_enabled
+      expose :only_allow_merge_if_all_discussions_are_resolved
     end
 
     class Member < UserBasic
       expose :access_level do |user, options|
-        member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+        member = options[:member] || options[:source].members.find_by(user_id: user.id)
         member.access_level
       end
+      expose :expires_at do |user, options|
+        member = options[:member] || options[:source].members.find_by(user_id: user.id)
+        member.expires_at
+      end
     end
 
     class AccessRequester < UserBasic
       expose :requested_at do |user, options|
-        access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
+        access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id)
         access_requester.requested_at
       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
     end
 
     class GroupDetail < Group
@@ -121,7 +138,7 @@ module API
       expose :name
 
       expose :commit do |repo_branch, options|
-        options[:project].repository.commit(repo_branch.target)
+        options[:project].repository.commit(repo_branch.dereferenced_target)
       end
 
       expose :protected do |repo_branch, options|
@@ -173,6 +190,10 @@ module API
 
       # TODO (rspeicher): Deprecated; remove in 9.0
       expose(:expires_at) { |snippet| nil }
+
+      expose :web_url do |snippet, options|
+        Gitlab::UrlBuilder.build(snippet)
+      end
     end
 
     class ProjectEntity < Grape::Entity
@@ -202,6 +223,11 @@ module API
       expose :user_notes_count
       expose :upvotes, :downvotes
       expose :due_date
+      expose :confidential
+
+      expose :web_url do |issue, options|
+        Gitlab::UrlBuilder.build(issue)
+      end
     end
 
     class ExternalIssue < Grape::Entity
@@ -219,12 +245,18 @@ module API
       expose :milestone, using: Entities::Milestone
       expose :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])
       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 MergeRequestChanges < MergeRequest
@@ -233,6 +265,19 @@ module API
       end
     end
 
+    class MergeRequestDiff < Grape::Entity
+      expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+        :created_at, :merge_request_id, :state, :real_size
+    end
+
+    class MergeRequestDiffFull < MergeRequestDiff
+      expose :commits, using: Entities::RepoCommit
+
+      expose :diffs, using: Entities::RepoDiff do |compare, _|
+        compare.raw_diffs(all_diffs: true).to_a
+      end
+    end
+
     class SSHKey < Grape::Entity
       expose :id, :title, :key, :created_at, :can_push
     end
@@ -298,7 +343,7 @@ module API
     end
 
     class ProjectGroupLink < Grape::Entity
-      expose :id, :project_id, :group_id, :group_access
+      expose :id, :project_id, :group_id, :group_access, :expires_at
     end
 
     class Todo < Grape::Entity
@@ -334,7 +379,7 @@ module API
       expose :access_level
       expose :notification_level do |member, options|
         if member.notification_setting
-          NotificationSetting.levels[member.notification_setting.level]
+          ::NotificationSetting.levels[member.notification_setting.level]
         end
       end
     end
@@ -345,6 +390,21 @@ module API
     class GroupAccess < MemberAccess
     end
 
+    class NotificationSetting < Grape::Entity
+      expose :level
+      expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
+        ::NotificationSetting::EMAIL_EVENTS.each do |event|
+          expose event
+        end
+      end
+    end
+
+    class GlobalNotificationSetting < NotificationSetting
+      expose :notification_email do |notification_setting, options|
+        notification_setting.user.notification_email
+      end
+    end
+
     class ProjectService < Grape::Entity
       expose :id, :title, :created_at, :updated_at, :active
       expose :push_events, :issues_events, :merge_requests_events
@@ -372,15 +432,34 @@ module API
       end
     end
 
-    class Label < Grape::Entity
-      expose :name, :color, :description
+    class LabelBasic < Grape::Entity
+      expose :id, :name, :color, :description
+    end
+
+    class Label < LabelBasic
       expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
+      expose :priority do |label, options|
+        label.priority(options[:project])
+      end
 
       expose :subscribed do |label, options|
         label.subscribed?(options[:current_user])
       end
     end
 
+    class List < Grape::Entity
+      expose :id
+      expose :label, using: Entities::LabelBasic
+      expose :position
+    end
+
+    class Board < Grape::Entity
+      expose :id
+      expose :lists, using: Entities::List do |board|
+        board.lists.destroyable
+      end
+    end
+
     class Compare < Grape::Entity
       expose :commit, using: Entities::RepoCommit do |compare, options|
         Commit.decorate(compare.commits, nil).last
@@ -434,6 +513,9 @@ module API
       expose :after_sign_out_path
       expose :container_registry_token_expire_delay
       expose :repository_storage
+      expose :repository_storages
+      expose :koding_enabled
+      expose :koding_url
     end
 
     class Release < Grape::Entity
@@ -445,7 +527,7 @@ module API
       expose :name, :message
 
       expose :commit do |repo_tag, options|
-        options[:project].repository.commit(repo_tag.target)
+        options[:project].repository.commit(repo_tag.dereferenced_target)
       end
 
       expose :release, using: Entities::Release do |repo_tag, options|
@@ -485,6 +567,10 @@ module API
       expose :filename, :size
     end
 
+    class PipelineBasic < Grape::Entity
+      expose :id, :sha, :ref, :status
+    end
+
     class Build < Grape::Entity
       expose :id, :status, :stage, :name, :ref, :tag, :coverage
       expose :created_at, :started_at, :finished_at
@@ -492,6 +578,7 @@ module API
       expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
       expose :commit, with: RepoCommit
       expose :runner, with: Runner
+      expose :pipeline, with: PipelineBasic
     end
 
     class Trigger < Grape::Entity
@@ -502,10 +589,29 @@ module API
       expose :key, :value
     end
 
-    class Environment < Grape::Entity
+    class Pipeline < PipelineBasic
+      expose :before_sha, :tag, :yaml_errors
+
+      expose :user, with: Entities::UserBasic
+      expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+      expose :duration
+    end
+
+    class EnvironmentBasic < Grape::Entity
       expose :id, :name, :external_url
     end
 
+    class Environment < EnvironmentBasic
+      expose :project, using: Entities::Project
+    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
+    end
+
     class RepoLicense < Grape::Entity
       expose :key, :name, :nickname
       expose :featured, as: :popular
@@ -525,5 +631,10 @@ module API
     class Template < Grape::Entity
       expose :name, :content
     end
+
+    class BroadcastMessage < Grape::Entity
+      expose :id, :message, :starts_at, :ends_at, :color, :font
+      expose :active?, as: :active
+    end
   end
 end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index c1d86f313b0a16c70bd4869749741e2260fafe7d..96510e651a319df34d589821566da19b4294795a 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -11,14 +11,16 @@ module API
           target_branch: attrs[:branch_name],
           commit_message: attrs[:commit_message],
           file_content: attrs[:content],
-          file_content_encoding: attrs[:encoding]
+          file_content_encoding: attrs[:encoding],
+          author_email: attrs[:author_email],
+          author_name: attrs[:author_name]
         }
       end
 
       def commit_response(attrs)
         {
           file_path: attrs[:file_path],
-          branch_name: attrs[:branch_name],
+          branch_name: attrs[:branch_name]
         }
       end
     end
@@ -96,7 +98,7 @@ module API
         authorize! :push_code, user_project
 
         required_attributes! [:file_path, :branch_name, :content, :commit_message]
-        attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+        attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
         result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute
 
         if result[:status] == :success
@@ -122,7 +124,7 @@ module API
         authorize! :push_code, user_project
 
         required_attributes! [:file_path, :branch_name, :content, :commit_message]
-        attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+        attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
         result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute
 
         if result[:status] == :success
@@ -149,7 +151,7 @@ module API
         authorize! :push_code, user_project
 
         required_attributes! [:file_path, :branch_name, :commit_message]
-        attrs = attributes_for_keys [:file_path, :branch_name, :commit_message]
+        attrs = attributes_for_keys [:file_path, :branch_name, :commit_message, :author_email, :author_name]
         result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute
 
         if result[:status] == :success
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9d8b8d737a9b06faf7a8d1589b9e57369d53fbda..40644fc2adf605a27986f8bda9f4de7501035485 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -6,34 +6,52 @@ module API
     resource :groups do
       # Get a groups list
       #
+      # Parameters:
+      #   skip_groups (optional) - Array of group ids to exclude from list
+      #   all_available (optional, boolean) - Show all group that you have access to
       # Example Request:
       #  GET /groups
       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 = paginate @groups
         present @groups, with: Entities::Group
       end
 
+      # Get list of owned groups for authenticated user
+      #
+      # Example Request:
+      #   GET /groups/owned
+      get '/owned' do
+        @groups = current_user.owned_groups
+        @groups = paginate @groups
+        present @groups, with: Entities::Group, user: current_user
+      end
+
       # Create group. Available only for users who can create groups.
       #
       # Parameters:
-      #   name (required)             - The name of the group
-      #   path (required)             - The path of the group
-      #   description (optional)      - The description of the group
-      #   visibility_level (optional) - The visibility level of the group
+      #   name (required)                   - The name of the group
+      #   path (required)                   - The path of the group
+      #   description (optional)            - The description of the group
+      #   visibility_level (optional)       - The visibility level of the group
+      #   lfs_enabled (optional)            - Enable/disable LFS for the projects in this group
+      #   request_access_enabled (optional) - Allow users to request member access
       # Example Request:
       #   POST /groups
       post do
-        authorize! :create_group, current_user
+        authorize! :create_group
         required_attributes! [:name, :path]
 
-        attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+        attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
         @group = Group.new(attrs)
 
         if @group.save
@@ -47,17 +65,19 @@ module API
       # Update group. Available only for users who can administrate groups.
       #
       # Parameters:
-      #   id (required)               - The ID of a group
-      #   path (optional)             - The path of the group
-      #   description (optional)      - The description of the group
-      #   visibility_level (optional) - The visibility level of the group
+      #   id (required)                     - The ID of a group
+      #   path (optional)                   - The path of the group
+      #   description (optional)            - The description of the group
+      #   visibility_level (optional)       - The visibility level of the group
+      #   lfs_enabled (optional)            - Enable/disable LFS for the projects in this group
+      #   request_access_enabled (optional) - Allow users to request member access
       # Example Request:
       #   PUT /groups/:id
       put ':id' do
         group = find_group(params[:id])
         authorize! :admin_group, group
 
-        attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+        attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
 
         if ::Groups::UpdateService.new(group, current_user, attrs).execute
           present group, with: Entities::GroupDetail
@@ -97,7 +117,7 @@ module API
         group = find_group(params[:id])
         projects = GroupProjectsFinder.new(group).execute(current_user)
         projects = paginate projects
-        present projects, with: Entities::Project
+        present projects, with: Entities::Project, user: current_user
       end
 
       # Transfer a project to the Group namespace
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index d0469d6602d0df0feb595a1285a0c8420703c02b..3c9d7b1aaef9fad3113649225b15bffe0ca8349c 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,24 +1,39 @@
 module API
   module Helpers
+    include Gitlab::Utils
+
     PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
     PRIVATE_TOKEN_PARAM = :private_token
     SUDO_HEADER = "HTTP_SUDO"
     SUDO_PARAM = :sudo
 
-    def to_boolean(value)
-      return true if value =~ /^(true|t|yes|y|1|on)$/i
-      return false if value =~ /^(false|f|no|n|0|off)$/i
+    def private_token
+      params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+    end
+
+    def warden
+      env['warden']
+    end
 
-      nil
+    # Check the Rails session for valid authentication details
+    #
+    # Until CSRF protection is added to the API, disallow this method for
+    # state-changing endpoints
+    def find_user_from_warden
+      warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
     end
 
     def find_user_by_private_token
-      token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
-      User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+      token = private_token
+      return nil unless token.present?
+
+      User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
     end
 
     def current_user
-      @current_user ||= (find_user_by_private_token || doorkeeper_guard)
+      @current_user ||= find_user_by_private_token
+      @current_user ||= doorkeeper_guard
+      @current_user ||= find_user_from_warden
 
       unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
         return nil
@@ -51,6 +66,10 @@ module API
       @project ||= find_project(params[:id])
     end
 
+    def available_labels
+      @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
+    end
+
     def find_project(id)
       project = Project.find_with_namespace(id) || Project.find_by(id: id)
 
@@ -98,7 +117,7 @@ module API
     end
 
     def find_project_label(id)
-      label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
+      label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
       label || not_found!('Label')
     end
 
@@ -129,7 +148,7 @@ module API
       forbidden! unless current_user.is_admin?
     end
 
-    def authorize!(action, subject)
+    def authorize!(action, subject = nil)
       forbidden! unless can?(current_user, action, subject)
     end
 
@@ -148,7 +167,7 @@ module API
     end
 
     def can?(object, action, subject)
-      abilities.allowed?(object, action, subject)
+      Ability.allowed?(object, action, subject)
     end
 
     # Checks the occurrences of required attributes, each attribute must be present in the params hash
@@ -177,16 +196,11 @@ module API
     def validate_label_params(params)
       errors = {}
 
-      if params[:labels].present?
-        params[:labels].split(',').each do |label_name|
-          label = user_project.labels.create_with(
-            color: Label::DEFAULT_COLOR).find_or_initialize_by(
-              title: label_name.strip)
+      params[:labels].to_s.split(',').each do |label_name|
+        label = available_labels.find_or_initialize_by(title: label_name.strip)
+        next if label.valid?
 
-          if label.invalid?
-            errors[label.title] = label.errors
-          end
-        end
+        errors[label.title] = label.errors
       end
 
       errors
@@ -269,6 +283,10 @@ module API
       render_api_error!('304 Not Modified', 304)
     end
 
+    def no_content!
+      render_api_error!('204 No Content', 204)
+    end
+
     def render_validation_error!(model)
       if model.errors.any?
         render_api_error!(model.errors.messages || '400 Bad Request', 400)
@@ -279,6 +297,24 @@ module API
       error!({ 'message' => message }, status)
     end
 
+    def handle_api_exception(exception)
+      if sentry_enabled? && report_exception?(exception)
+        define_params_for_grape_middleware
+        sentry_context
+        Raven.capture_exception(exception)
+      end
+
+      # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+      trace = exception.backtrace
+
+      message = "\n#{exception.class} (#{exception.message}):\n"
+      message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+      message << "  " << trace.join("\n  ")
+
+      API.logger.add Logger::FATAL, message
+      rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
+    end
+
     # Projects helpers
 
     def filter_projects(projects)
@@ -390,16 +426,8 @@ module API
       links.join(', ')
     end
 
-    def abilities
-      @abilities ||= begin
-                       abilities = Six.new
-                       abilities << Ability
-                       abilities
-                     end
-    end
-
     def secret_token
-      File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+      Gitlab::Shell.secret_token
     end
 
     def send_git_blob(repository, blob)
@@ -419,5 +447,19 @@ module API
         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
+      self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys }
+    end
+
+    # We could get a Grape or a standard Ruby exception. We should only report anything that
+    # is clearly an error.
+    def report_exception?(exception)
+      return true unless exception.respond_to?(:status)
+
+      exception.status == 500
+    end
   end
 end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d8e9ac406c4c4067df971508e6c52af123a35224..ccf181402f98f0a77b2064a625e50ca476f79134 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -17,15 +17,20 @@ module API
       #
 
       helpers do
+        def project_path
+          @project_path ||= begin
+            project_path = params[:project].sub(/\.git\z/, '')
+            Repository.remove_storage_from_path(project_path)
+          end
+        end
+
         def wiki?
-          @wiki ||= params[:project].end_with?('.wiki') &&
-            !Project.find_with_namespace(params[:project])
+          @wiki ||= project_path.end_with?('.wiki') &&
+            !Project.find_with_namespace(project_path)
         end
 
         def project
           @project ||= begin
-            project_path = params[:project]
-
             # Check for *.wiki repositories.
             # Strip out the .wiki from the pathname before finding the
             # project. This applies the correct project permissions to
@@ -35,6 +40,14 @@ module API
             Project.find_with_namespace(project_path)
           end
         end
+
+        def ssh_authentication_abilities
+          [
+            :read_project,
+            :download_code,
+            :push_code
+          ]
+        end
       end
 
       post "/allowed" do
@@ -51,9 +64,9 @@ module API
 
         access =
           if wiki?
-            Gitlab::GitAccessWiki.new(actor, project, protocol)
+            Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
           else
-            Gitlab::GitAccess.new(actor, project, protocol)
+            Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
           end
 
         access_status = access.check(params[:action], params[:changes])
@@ -74,6 +87,19 @@ module API
         response
       end
 
+      post "/lfs_authenticate" do
+        status 200
+
+        key = Key.find(params[:key_id])
+        token_handler = Gitlab::LfsToken.new(key)
+
+        {
+          username: token_handler.actor_name,
+          lfs_token: token_handler.token,
+          repository_http_path: project.http_url_to_repo
+        }
+      end
+
       get "/merge_request_urls" do
         ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
       end
@@ -101,6 +127,35 @@ module API
           {}
         end
       end
+
+      post '/two_factor_recovery_codes' do
+        status 200
+
+        key = Key.find_by(id: params[:key_id])
+
+        unless key
+          return { 'success' => false, 'message' => 'Could not find the given key' }
+        end
+
+        if key.is_a?(DeployKey)
+          return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+        end
+
+        user = key.user
+
+        unless user
+          return { success: false, message: 'Could not find a user for the given key' }
+        end
+
+        unless user.two_factor_enabled?
+          return { success: false, message: 'Two-factor authentication is not enabled for this user' }
+        end
+
+        codes = user.generate_otp_backup_codes!
+        user.save!
+
+        { success: true, recovery_codes: codes }
+      end
     end
   end
 end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 077258faee19235a01025d992e04fe9e528b6cd5..c9689e6f8ef17b72219d67cef673bef542b67a0c 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -41,7 +41,8 @@ module API
         issues = current_user.issues.inc_notes_with_associations
         issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
         issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
-        issues.reorder(issuable_order_by => issuable_sort)
+        issues = issues.reorder(issuable_order_by => issuable_sort)
+
         present paginate(issues), with: Entities::Issue, current_user: current_user
       end
     end
@@ -73,7 +74,11 @@ module API
         params[:group_id] = group.id
         params[:milestone_title] = params.delete(:milestone)
         params[:label_name] = params.delete(:labels)
-        params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort]
+
+        if params[:order_by] || params[:sort]
+          # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example)
+          params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}"
+        end
 
         issues = IssuesFinder.new(current_user, params).execute
 
@@ -113,7 +118,8 @@ module API
           issues = filter_issues_milestone(issues, params[:milestone])
         end
 
-        issues.reorder(issuable_order_by => issuable_sort)
+        issues = issues.reorder(issuable_order_by => issuable_sort)
+
         present paginate(issues), with: Entities::Issue, current_user: current_user
       end
 
@@ -140,12 +146,13 @@ module API
       #   labels (optional)       - The labels of an issue
       #   created_at (optional)   - Date time string, ISO 8601 formatted
       #   due_date (optional)     - Date time string in the format YEAR-MONTH-DAY
+      #   confidential (optional) - Boolean parameter if the issue should be confidential
       # Example Request:
       #   POST /projects/:id/issues
       post ':id/issues' do
         required_attributes! [:title]
 
-        keys = [:title, :description, :assignee_id, :milestone_id, :due_date]
+        keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential]
         keys << :created_at if current_user.admin? || user_project.owner == current_user
         attrs = attributes_for_keys(keys)
 
@@ -154,21 +161,19 @@ module API
           render_api_error!({ labels: errors }, 400)
         end
 
-        project = user_project
+        attrs[:labels] = params[:labels] if params[:labels]
+
+        # Convert and filter out invalid confidential flags
+        attrs['confidential'] = to_boolean(attrs['confidential'])
+        attrs.delete('confidential') if attrs['confidential'].nil?
 
-        issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
+        issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
 
         if issue.spam?
           render_api_error!({ error: 'Spam detected' }, 400)
         end
 
         if issue.valid?
-          # Find or create labels and attach to issue. Labels are valid because
-          # we already checked its name, so there can't be an error here
-          if params[:labels].present?
-            issue.add_labels_by_names(params[:labels].split(','))
-          end
-
           present issue, with: Entities::Issue, current_user: current_user
         else
           render_validation_error!(issue)
@@ -188,12 +193,13 @@ module API
       #   state_event (optional) - The state event of an issue (close|reopen)
       #   updated_at (optional) - Date time string, ISO 8601 formatted
       #   due_date (optional)     - Date time string in the format YEAR-MONTH-DAY
+      #   confidential (optional) - Boolean parameter if the issue should be confidential
       # Example Request:
       #   PUT /projects/:id/issues/:issue_id
       put ':id/issues/:issue_id' do
         issue = user_project.issues.find(params[:issue_id])
         authorize! :update_issue, issue
-        keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date]
+        keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential]
         keys << :updated_at if current_user.admin? || user_project.owner == current_user
         attrs = attributes_for_keys(keys)
 
@@ -202,17 +208,15 @@ module API
           render_api_error!({ labels: errors }, 400)
         end
 
+        attrs[:labels] = params[:labels] if params[:labels]
+
+        # Convert and filter out invalid confidential flags
+        attrs['confidential'] = to_boolean(attrs['confidential'])
+        attrs.delete('confidential') if attrs['confidential'].nil?
+
         issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
 
         if issue.valid?
-          # Find or create labels and attach to issue. Labels are valid because
-          # we already checked its name, so there can't be an error here
-          if params[:labels] && can?(current_user, :admin_issue, user_project)
-            issue.remove_labels
-            # Create and add labels to the new created issue
-            issue.add_labels_by_names(params[:labels].split(','))
-          end
-
           present issue, with: Entities::Issue, current_user: current_user
         else
           render_validation_error!(issue)
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 2b723b7950472014dcb7d6c376edb27f8881307b..767f27ef334122f129ede6bc6aaccc08cb7b5e0c 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -4,10 +4,9 @@ module API
     before { authenticate! }
 
     resource :keys do
-      # Get single ssh key by id. Only available to admin users.
-      #
-      # Example Request:
-      #   GET /keys/:id
+      desc 'Get single ssh key by id. Only available to admin users' do
+        success Entities::SSHKeyWithUser
+      end
       get ":id" do
         authenticated_as_admin!
 
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index c806829d69e52610352fc9e4d9fa4af02c249956..97218054f3764cb2b01048e4cc985f3ced2b6a4c 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -3,97 +3,97 @@ module API
   class Labels < Grape::API
     before { authenticate! }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get all labels of the project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      # Example Request:
-      #   GET /projects/:id/labels
+      desc 'Get all labels of the project' do
+        success Entities::Label
+      end
       get ':id/labels' do
-        present user_project.labels, with: Entities::Label, current_user: current_user
+        present available_labels, with: Entities::Label, current_user: current_user, project: user_project
       end
 
-      # Creates a new label
-      #
-      # Parameters:
-      #   id    (required)       - The ID of a project
-      #   name  (required)       - The name of the label to be created
-      #   color (required)       - Color of the label given in 6-digit hex
-      #                            notation with leading '#' sign (e.g. #FFAABB)
-      #   description (optional) - The description of label to be created
-      # Example Request:
-      #   POST /projects/:id/labels
+      desc 'Create a new label' do
+        success Entities::Label
+      end
+      params do
+        requires :name, type: String, desc: 'The name of the label to be created'
+        requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+        optional :description, type: String, desc: 'The description of label to be created'
+        optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
+      end
       post ':id/labels' do
         authorize! :admin_label, user_project
-        required_attributes! [:name, :color]
-
-        attrs = attributes_for_keys [:name, :color, :description]
-        label = user_project.find_label(attrs[:name])
 
+        label = available_labels.find_by(title: params[:name])
         conflict!('Label already exists') if label
 
-        label = user_project.labels.create(attrs)
+        priority = params.delete(:priority)
+        label_params = declared(params,
+                                include_parent_namespaces: false,
+                                include_missing: false).to_h
+        label = user_project.labels.create(label_params)
 
         if label.valid?
-          present label, with: Entities::Label, current_user: current_user
+          label.prioritize!(user_project, priority) if priority
+          present label, with: Entities::Label, current_user: current_user, project: user_project
         else
           render_validation_error!(label)
         end
       end
 
-      # Deletes an existing label
-      #
-      # Parameters:
-      #   id    (required) - The ID of a project
-      #   name  (required) - The name of the label to be deleted
-      #
-      # Example Request:
-      #   DELETE /projects/:id/labels
+      desc 'Delete an existing label' do
+        success 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
-        required_attributes! [:name]
 
-        label = user_project.find_label(params[:name])
+        label = user_project.labels.find_by(title: params[:name])
         not_found!('Label') unless label
 
-        label.destroy
+        present label.destroy, with: Entities::Label, current_user: current_user, project: user_project
       end
 
-      # Updates an existing label. At least one optional parameter is required.
-      #
-      # Parameters:
-      #   id        (required)   - The ID of a project
-      #   name      (required)   - The name of the label to be deleted
-      #   new_name  (optional)   - The new name of the label
-      #   color     (optional)   - Color of the label given in 6-digit hex
-      #                            notation with leading '#' sign (e.g. #FFAABB)
-      #   description (optional) - The description of label to be created
-      # Example Request:
-      #   PUT /projects/:id/labels
+      desc 'Update an existing label. At least one optional parameter is required.' do
+        success Entities::Label
+      end
+      params do
+        requires :name,  type: String, desc: 'The name of the label to be updated'
+        optional :new_name, type: String, desc: 'The new name of the label'
+        optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+        optional :description, type: String, desc: 'The new description of label'
+        optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
+        at_least_one_of :new_name, :color, :description, :priority
+      end
       put ':id/labels' do
         authorize! :admin_label, user_project
-        required_attributes! [:name]
 
-        label = user_project.find_label(params[:name])
+        label = user_project.labels.find_by(title: params[:name])
         not_found!('Label not found') unless label
 
-        attrs = attributes_for_keys [:new_name, :color, :description]
-
-        if attrs.empty?
-          render_api_error!('Required parameters "new_name" or "color" ' \
-                            'missing',
-                            400)
-        end
-
+        update_priority = params.key?(:priority)
+        priority = params.delete(:priority)
+        label_params = declared(params,
+                                include_parent_namespaces: false,
+                                include_missing: false).to_h
         # Rename new name to the actual label attribute name
-        attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name)
+        label_params[:name] = label_params.delete('new_name') if label_params.key?('new_name')
 
-        if label.update(attrs)
-          present label, with: Entities::Label, current_user: current_user
-        else
-          render_validation_error!(label)
+        render_validation_error!(label) unless label.update(label_params)
+
+        if update_priority
+          if priority.nil?
+            label.unprioritize!(user_project)
+          else
+            label.prioritize!(user_project, priority)
+          end
         end
+
+        present label, with: Entities::Label, current_user: current_user, project: user_project
       end
     end
   end
diff --git a/lib/api/license_templates.rb b/lib/api/license_templates.rb
deleted file mode 100644
index d0552299ed0788d6b19ff97bbea54f7793efce18..0000000000000000000000000000000000000000
--- a/lib/api/license_templates.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-module API
-  # License Templates API
-  class LicenseTemplates < Grape::API
-    PROJECT_TEMPLATE_REGEX =
-      /[\<\{\[]
-        (project|description|
-        one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
-      [\>\}\]]/xi.freeze
-    YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
-    FULLNAME_TEMPLATE_REGEX =
-      /[\<\{\[]
-        (fullname|name\sof\s(author|copyright\sowner))
-      [\>\}\]]/xi.freeze
-
-    # Get the list of the available license templates
-    #
-    # Parameters:
-    #   popular - Filter licenses to only the popular ones
-    #
-    # Example Request:
-    #   GET /licenses
-    #   GET /licenses?popular=1
-    get 'licenses' do
-      options = {
-        featured: params[:popular].present? ? true : nil
-      }
-      present Licensee::License.all(options), with: Entities::RepoLicense
-    end
-
-    # Get text for specific license
-    #
-    # Parameters:
-    #   key (required) - The key of a license
-    #   project        - Copyrighted project name
-    #   fullname       - Full name of copyright holder
-    #
-    # Example Request:
-    #   GET /licenses/mit
-    #
-    get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do
-      required_attributes! [:key]
-
-      not_found!('License') unless Licensee::License.find(params[:key])
-
-      # We create a fresh Licensee::License object since we'll modify its
-      # content in place below.
-      license = Licensee::License.new(params[:key])
-
-      license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
-      license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
-
-      fullname = params[:fullname].presence || current_user.try(:name)
-      license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
-
-      present license, with: Entities::RepoLicense
-    end
-  end
-end
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae43a4a32376bcee210bb07c72ed683352707090
--- /dev/null
+++ b/lib/api/lint.rb
@@ -0,0 +1,21 @@
+module API
+  class Lint < Grape::API
+    namespace :ci do
+      desc 'Validation of .gitlab-ci.yml content'
+      params do
+        requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
+      end
+      post '/lint' do
+        error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
+
+        status 200
+
+        if error.blank?
+          { status: 'valid', errors: [] }
+        else
+          { status: 'invalid', errors: [error] }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2fae83f60b278aace40315c85f401c41789ce776..b80818f0eb6272a24f023474632fb6a238b7b22e 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -5,35 +5,32 @@ module API
     helpers ::API::Helpers::MembersHelpers
 
     %w[group project].each do |source_type|
+      params do
+        requires :id, type: String, desc: "The #{source_type} ID"
+      end
       resource source_type.pluralize do
-        # Get a list of group/project members viewable by the authenticated user.
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   query         - Query string
-        #
-        # Example Request:
-        #   GET /groups/:id/members
-        #   GET /projects/:id/members
+        desc 'Gets a list of group or project members viewable by the authenticated user.' do
+          success Entities::Member
+        end
+        params do
+          optional :query, type: String, desc: 'A query string to search for members'
+        end
         get ":id/members" do
           source = find_source(source_type, params[:id])
 
-          members = source.members.includes(:user)
-          members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
-          members = paginate(members)
+          users = source.users
+          users = users.merge(User.search(params[:query])) if params[:query]
+          users = paginate(users)
 
-          present members.map(&:user), with: Entities::Member, members: members
+          present users, with: Entities::Member, source: source
         end
 
-        # Get a group/project member
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the member
-        #
-        # Example Request:
-        #   GET /groups/:id/members/:user_id
-        #   GET /projects/:id/members/:user_id
+        desc 'Gets a member of a group or project.' do
+          success Entities::Member
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the member'
+        end
         get ":id/members/:user_id" do
           source = find_source(source_type, params[:id])
 
@@ -43,47 +40,34 @@ module API
           present member.user, with: Entities::Member, member: member
         end
 
-        # Add a new group/project member
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the new member
-        #   access_level (required) - A valid access level
-        #
-        # Example Request:
-        #   POST /groups/:id/members
-        #   POST /projects/:id/members
+        desc 'Adds a member to a group or project.' do
+          success Entities::Member
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the new member'
+          requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+          optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+        end
         post ":id/members" do
           source = find_source(source_type, params[:id])
           authorize_admin_source!(source_type, source)
-          required_attributes! [:user_id, :access_level]
-
-          access_requester = source.requesters.find_by(user_id: params[:user_id])
-          if access_requester
-            # We pass current_user = access_requester so that the requester doesn't
-            # receive a "access denied" email
-            ::Members::DestroyService.new(access_requester, access_requester.user).execute
-          end
 
           member = source.members.find_by(user_id: params[:user_id])
 
-          # This is to ensure back-compatibility but 409 behavior should be used
-          # for both project and group members in 9.0!
+          # We need this explicit check because `source.add_user` doesn't
+          # currently return the member created so it would return 201 even if
+          # the member already existed...
+          # The `source_type == 'group'` check is to ensure back-compatibility
+          # but 409 behavior should be used for both project and group members in 9.0!
           conflict!('Member already exists') if source_type == 'group' && member
 
           unless member
-            source.add_user(params[:user_id], params[:access_level], current_user)
-            member = source.members.find_by(user_id: params[:user_id])
+            member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
           end
 
-          if member
+          if member.persisted? && member.valid?
             present member.user, with: Entities::Member, member: member
           else
-            # Since `source.add_user` doesn't return a member object, we have to
-            # build a new one and populate its errors in order to render them.
-            member = source.members.build(attributes_for_keys([:user_id, :access_level]))
-            member.valid? # populate the errors
-
             # 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)
@@ -91,24 +75,22 @@ module API
           end
         end
 
-        # Update a group/project member
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the member
-        #   access_level (required) - A valid access level
-        #
-        # Example Request:
-        #   PUT /groups/:id/members/:user_id
-        #   PUT /projects/:id/members/:user_id
+        desc 'Updates a member of a group or project.' do
+          success Entities::Member
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the new member'
+          requires :access_level, type: Integer, desc: 'A valid access level'
+          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])
           authorize_admin_source!(source_type, source)
-          required_attributes! [:user_id, :access_level]
 
           member = source.members.find_by!(user_id: params[:user_id])
+          attrs = attributes_for_keys [:access_level, :expires_at]
 
-          if member.update_attributes(access_level: params[:access_level])
+          if member.update_attributes(attrs)
             present member.user, with: Entities::Member, member: member
           else
             # This is to ensure back-compatibility but 400 behavior should be used
@@ -118,18 +100,12 @@ module API
           end
         end
 
-        # Remove a group/project member
-        #
-        # Parameters:
-        #   id (required) - The group/project ID
-        #   user_id (required) - The user ID of the member
-        #
-        # Example Request:
-        #   DELETE /groups/:id/members/:user_id
-        #   DELETE /projects/:id/members/:user_id
+        desc 'Removes a user from a group or project.'
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the member'
+        end
         delete ":id/members/:user_id" do
           source = find_source(source_type, params[:id])
-          required_attributes! [:user_id]
 
           # This is to ensure back-compatibility but find_by! should be used
           # in that casse in 9.0!
@@ -144,7 +120,7 @@ module API
           if member.nil?
             { message: "Access revoked", id: params[:user_id].to_i }
           else
-            ::Members::DestroyService.new(member, current_user).execute
+            ::Members::DestroyService.new(source, current_user, declared(params)).execute
 
             present member.user, with: Entities::Member, member: member
           end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..07435d78468ea343777a66303fc611bba9f9feb1
--- /dev/null
+++ b/lib/api/merge_request_diffs.rb
@@ -0,0 +1,45 @@
+module API
+  # MergeRequestDiff API
+  class MergeRequestDiffs < Grape::API
+    before { authenticate! }
+
+    resource :projects 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'
+      end
+
+      get ":id/merge_requests/:merge_request_id/versions" do
+        merge_request = user_project.merge_requests.
+          find(params[:merge_request_id])
+
+        authorize! :read_merge_request, merge_request
+        present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+      end
+
+      desc 'Get a single merge request diff version' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::MergeRequestDiffFull
+      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 :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 = user_project.merge_requests.
+          find(params[:merge_request_id])
+
+        authorize! :read_merge_request, merge_request
+        present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
+      end
+    end
+  end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 2b685621da9a0c4443e30962ae59a98b490f395e..bf8504e1101858d0e49f6eb951af05ea4bc821dc 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -86,14 +86,11 @@ module API
           render_api_error!({ labels: errors }, 400)
         end
 
+        attrs[:labels] = params[:labels] if params[:labels]
+
         merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
 
         if merge_request.valid?
-          # Find or create labels and attach to issue
-          if params[:labels].present?
-            merge_request.add_labels_by_names(params[:labels].split(","))
-          end
-
           present merge_request, with: Entities::MergeRequest, current_user: current_user
         else
           handle_merge_request_errors! merge_request.errors
@@ -195,15 +192,11 @@ module API
             render_api_error!({ labels: errors }, 400)
           end
 
+          attrs[:labels] = params[:labels] if params[:labels]
+
           merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
 
           if merge_request.valid?
-            # Find or create labels and attach to issue
-            unless params[:labels].nil?
-              merge_request.remove_labels
-              merge_request.add_labels_by_names(params[:labels].split(","))
-            end
-
             present merge_request, with: Entities::MergeRequest, current_user: current_user
           else
             handle_merge_request_errors! merge_request.errors
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 7a0cb7c99f32a33e216acd92c2da6e2f6fc6128c..8984cf8cdcd1b4ddb6d6b4d5e85f210fb5445be2 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -11,19 +11,25 @@ module API
         else milestones
         end
       end
+
+      params :optional_params do
+        optional :description, type: String, desc: 'The description of the milestone'
+        optional :due_date, type: String, desc: 'The due date of the milestone'
+      end
     end
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get a list of project milestones
-      #
-      # Parameters:
-      #   id (required)    - The ID of a project
-      #   state (optional) - Return "active" or "closed" milestones
-      # Example Request:
-      #   GET /projects/:id/milestones
-      #   GET /projects/:id/milestones?iid=42
-      #   GET /projects/:id/milestones?state=active
-      #   GET /projects/:id/milestones?state=closed
+      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: Integer, desc: 'The IID of the milestone'
+      end
       get ":id/milestones" do
         authorize! :read_milestone, user_project
 
@@ -34,34 +40,31 @@ module API
         present paginate(milestones), with: Entities::Milestone
       end
 
-      # Get a single project milestone
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   milestone_id (required) - The ID of a project milestone
-      # Example Request:
-      #   GET /projects/:id/milestones/:milestone_id
+      desc 'Get a single project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+      end
       get ":id/milestones/:milestone_id" do
         authorize! :read_milestone, user_project
 
-        @milestone = user_project.milestones.find(params[:milestone_id])
-        present @milestone, with: Entities::Milestone
+        milestone = user_project.milestones.find(params[:milestone_id])
+        present milestone, with: Entities::Milestone
       end
 
-      # Create a new project milestone
-      #
-      # Parameters:
-      #   id (required) - The ID of the project
-      #   title (required) - The title of the milestone
-      #   description (optional) - The description of the milestone
-      #   due_date (optional) - The due date of the milestone
-      # Example Request:
-      #   POST /projects/:id/milestones
+      desc 'Create a new project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :title, type: String, desc: 'The title of the milestone'
+        use :optional_params
+      end
       post ":id/milestones" do
         authorize! :admin_milestone, user_project
-        required_attributes! [:title]
-        attrs = attributes_for_keys [:title, :description, :due_date]
-        milestone = ::Milestones::CreateService.new(user_project, current_user, attrs).execute
+        milestone_params = declared(params, include_parent_namespaces: false)
+
+        milestone = ::Milestones::CreateService.new(user_project, current_user, milestone_params).execute
 
         if milestone.valid?
           present milestone, with: Entities::Milestone
@@ -70,22 +73,23 @@ module API
         end
       end
 
-      # Update an existing project milestone
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   milestone_id (required) - The ID of a project milestone
-      #   title (optional) - The title of a milestone
-      #   description (optional) - The description of a milestone
-      #   due_date (optional) - The due date of a milestone
-      #   state_event (optional) - The state event of the milestone (close|activate)
-      # Example Request:
-      #   PUT /projects/:id/milestones/:milestone_id
+      desc 'Update an existing project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+        optional :title, type: String, desc: 'The title of the milestone'
+        optional :state_event, type: String, values: %w[close activate],
+                               desc: 'The state event of the milestone '
+        use :optional_params
+        at_least_one_of :title, :description, :due_date, :state_event
+      end
       put ":id/milestones/:milestone_id" do
         authorize! :admin_milestone, user_project
-        attrs = attributes_for_keys [:title, :description, :due_date, :state_event]
-        milestone = user_project.milestones.find(params[:milestone_id])
-        milestone = ::Milestones::UpdateService.new(user_project, current_user, attrs).execute(milestone)
+        milestone_params = declared(params, include_parent_namespaces: false, include_missing: false)
+
+        milestone = user_project.milestones.find(milestone_params.delete(:milestone_id))
+        milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
 
         if milestone.valid?
           present milestone, with: Entities::Milestone
@@ -94,22 +98,20 @@ module API
         end
       end
 
-      # Get all issues for a single project milestone
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   milestone_id (required) - The ID of a project milestone
-      # Example Request:
-      #   GET /projects/:id/milestones/:milestone_id/issues
+      desc 'Get all issues for a single project milestone' do
+        success Entities::Issue
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+      end
       get ":id/milestones/:milestone_id/issues" do
         authorize! :read_milestone, user_project
 
-        @milestone = user_project.milestones.find(params[:milestone_id])
+        milestone = user_project.milestones.find(params[:milestone_id])
 
         finder_params = {
           project_id: user_project.id,
-          milestone_title: @milestone.title,
-          state: 'all'
+          milestone_title: milestone.title
         }
 
         issues = IssuesFinder.new(current_user, finder_params).execute
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 50d3729449e1f9d2a223acdcfb33a021b92ab1bd..fe981d7b9fa6b560c3622b614b7f7acf8c3eafc5 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -4,20 +4,18 @@ module API
     before { authenticate! }
 
     resource :namespaces do
-      # Get a namespaces list
-      #
-      # Example Request:
-      #  GET /namespaces
+      desc 'Get a namespaces list' do
+        success Entities::Namespace
+      end
+      params do
+        optional :search, type: String, desc: "Search query for namespaces"
+      end
       get do
-        @namespaces = if current_user.admin
-                        Namespace.all
-                      else
-                        current_user.namespaces
-                      end
-        @namespaces = @namespaces.search(params[:search]) if params[:search].present?
-        @namespaces = paginate @namespaces
+        namespaces = current_user.admin ? Namespace.all : current_user.namespaces
+
+        namespaces = namespaces.search(params[:search]) if params[:search].present?
 
-        present @namespaces, with: Entities::Namespace
+        present paginate(namespaces), with: Entities::Namespace
       end
     end
   end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8bfa998dc531eb86b162f511281584f344f64a89..c5c214d4d1339f876c2551c72b6888ff45c26aed 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -83,12 +83,12 @@ module API
             opts[:created_at] = params[:created_at]
           end
 
-          @note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+          note = ::Notes::CreateService.new(user_project, current_user, opts).execute
 
-          if @note.valid?
-            present @note, with: Entities::Note
+          if note.valid?
+            present note, with: Entities::const_get(note.class.name)
           else
-            not_found!("Note #{@note.errors.messages}")
+            not_found!("Note #{note.errors.messages}")
           end
         end
 
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a70a7e7107390bf5851947a22dc6d39d2d9ec259
--- /dev/null
+++ b/lib/api/notification_settings.rb
@@ -0,0 +1,97 @@
+module API
+  # notification_settings API
+  class NotificationSettings < Grape::API
+    before { authenticate! }
+
+    helpers ::API::Helpers::MembersHelpers
+
+    resource :notification_settings do
+      desc 'Get global notification level settings and email, defaults to Participate' do
+        detail 'This feature was introduced in GitLab 8.12'
+        success Entities::GlobalNotificationSetting
+      end
+      get do
+        notification_setting = current_user.global_notification_setting
+
+        present notification_setting, with: Entities::GlobalNotificationSetting
+      end
+
+      desc 'Update global notification level settings and email, defaults to Participate' do
+        detail 'This feature was introduced in GitLab 8.12'
+        success Entities::GlobalNotificationSetting
+      end
+      params do
+        optional :level, type: String, desc: 'The global notification level'
+        optional :notification_email, type: String, desc: 'The email address to send notifications'
+        NotificationSetting::EMAIL_EVENTS.each do |event|
+          optional event, type: Boolean, desc: 'Enable/disable this notification'
+        end
+      end
+      put do
+        notification_setting = current_user.global_notification_setting
+
+        begin
+          notification_setting.transaction do
+            new_notification_email = params.delete(:notification_email)
+            declared_params = declared(params, include_missing: false).to_h
+
+            current_user.update(notification_email: new_notification_email) if new_notification_email
+            notification_setting.update(declared_params)
+          end
+        rescue ArgumentError => e # catch level enum error
+          render_api_error! e.to_s, 400
+        end
+
+        render_validation_error! current_user
+        render_validation_error! notification_setting
+        present notification_setting, with: Entities::GlobalNotificationSetting
+      end
+    end
+
+    %w[group project].each do |source_type|
+      resource source_type.pluralize 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])
+
+          notification_setting = current_user.notification_settings_for(source)
+
+          present notification_setting, with: Entities::NotificationSetting
+        end
+
+        desc "Update #{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'
+          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'
+          end
+        end
+        put ":id/notification_settings" do
+          source = find_source(source_type, params.delete(:id))
+          notification_setting = current_user.notification_settings_for(source)
+
+          begin
+            declared_params = declared(params, include_missing: false).to_h
+
+            notification_setting.update(declared_params)
+          rescue ArgumentError => e # catch level enum error
+            render_api_error! e.to_s, 400
+          end
+
+          render_validation_error! notification_setting
+          present notification_setting, with: Entities::NotificationSetting
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a0c8e1f2c0abe9a469d23e27e8dcb445af382e2
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,77 @@
+module API
+  class Pipelines < Grape::API
+    before { authenticate! }
+
+    params do
+      requires :id, type: String, desc: 'The project ID'
+    end
+    resource :projects do
+      desc 'Get all Pipelines of the project' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Pipeline
+      end
+      params do
+        optional :page,     type: Integer, desc: 'Page number of the current request'
+        optional :per_page, type: Integer, desc: 'Number of items per page'
+        optional :scope,    type: String, values: ['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
+      end
+
+      desc 'Gets a specific pipeline for the project' do
+        detail 'This feature was introduced in GitLab 8.11'
+        success Entities::Pipeline
+      end
+      params do
+        requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+      end
+      get ':id/pipelines/:pipeline_id' do
+        authorize! :read_pipeline, user_project
+
+        present pipeline, with: Entities::Pipeline
+      end
+
+      desc 'Retry failed builds in the pipeline' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Pipeline
+      end
+      params do
+        requires :pipeline_id, type: Integer,  desc: 'The pipeline ID'
+      end
+      post ':id/pipelines/:pipeline_id/retry' do
+        authorize! :update_pipeline, user_project
+
+        pipeline.retry_failed(current_user)
+
+        present pipeline, with: Entities::Pipeline
+      end
+
+      desc 'Cancel all builds in the pipeline' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Pipeline
+      end
+      params do
+        requires :pipeline_id, type: Integer,  desc: 'The pipeline ID'
+      end
+      post ':id/pipelines/:pipeline_id/cancel' do
+        authorize! :update_pipeline, user_project
+
+        pipeline.cancel_running
+
+        status 200
+        present pipeline.reload, with: Entities::Pipeline
+      end
+    end
+
+    helpers do
+      def pipeline
+        @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+      end
+    end
+  end
+end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 3f63cd678e8f3ae2fa5295911af695f6db7aeeba..eef343c2ac6c691b5ca0145f91e6e3d38b65fe26 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -1,110 +1,99 @@
 module API
   # Projects API
   class ProjectHooks < Grape::API
+    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_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
+
     before { authenticate! }
     before { authorize_admin_project }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get project hooks
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      # Example Request:
-      #   GET /projects/:id/hooks
+      desc 'Get project hooks' do
+        success Entities::ProjectHook
+      end
       get ":id/hooks" do
-        @hooks = paginate user_project.hooks
-        present @hooks, with: Entities::ProjectHook
+        hooks = paginate user_project.hooks
+
+        present hooks, with: Entities::ProjectHook
       end
 
-      # Get a project hook
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   hook_id (required) - The ID of a project hook
-      # Example Request:
-      #   GET /projects/:id/hooks/:hook_id
+      desc 'Get a project hook' do
+        success 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: Entities::ProjectHook
+        hook = user_project.hooks.find(params[:hook_id])
+        present hook, with: Entities::ProjectHook
       end
 
-      # Add hook to project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   url (required) - The hook URL
-      # Example Request:
-      #   POST /projects/:id/hooks
+      desc 'Add hook to project' do
+        success Entities::ProjectHook
+      end
+      params do
+        use :project_hook_properties
+      end
       post ":id/hooks" do
-        required_attributes! [:url]
-        attrs = attributes_for_keys [
-          :url,
-          :push_events,
-          :issues_events,
-          :merge_requests_events,
-          :tag_push_events,
-          :note_events,
-          :build_events,
-          :pipeline_events,
-          :enable_ssl_verification
-        ]
-        @hook = user_project.hooks.new(attrs)
+        new_hook_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
+        hook = user_project.hooks.new(new_hook_params)
 
-        if @hook.save
-          present @hook, with: Entities::ProjectHook
+        if hook.save
+          present hook, with: Entities::ProjectHook
         else
-          if @hook.errors[:url].present?
-            error!("Invalid url given", 422)
-          end
-          not_found!("Project hook #{@hook.errors.messages}")
+          error!("Invalid url given", 422) if hook.errors[:url].present?
+
+          not_found!("Project hook #{hook.errors.messages}")
         end
       end
 
-      # Update an existing project hook
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   hook_id (required) - The ID of a project hook
-      #   url (required) - The hook URL
-      # Example Request:
-      #   PUT /projects/:id/hooks/:hook_id
+      desc 'Update an existing project hook' do
+        success 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[:hook_id])
-        required_attributes! [:url]
-        attrs = attributes_for_keys [
-          :url,
-          :push_events,
-          :issues_events,
-          :merge_requests_events,
-          :tag_push_events,
-          :note_events,
-          :build_events,
-          :pipeline_events,
-          :enable_ssl_verification
-        ]
+        hook = user_project.hooks.find(params[:hook_id])
+
+        new_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
+        new_params.delete('hook_id')
 
-        if @hook.update_attributes attrs
-          present @hook, with: Entities::ProjectHook
+        if hook.update_attributes(new_params)
+          present hook, with: Entities::ProjectHook
         else
-          if @hook.errors[:url].present?
-            error!("Invalid url given", 422)
-          end
-          not_found!("Project hook #{@hook.errors.messages}")
+          error!("Invalid url given", 422) if hook.errors[:url].present?
+
+          not_found!("Project hook #{hook.errors.messages}")
         end
       end
 
-      # Deletes project hook. This is an idempotent function.
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   hook_id (required) - The ID of hook to delete
-      # Example Request:
-      #   DELETE /projects/:id/hooks/:hook_id
+      desc 'Deletes project hook' do
+        success 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
-        required_attributes! [:hook_id]
-
         begin
-          @hook = user_project.hooks.destroy(params[:hook_id])
+          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]}")
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 60cfc103afd27f94a8b5afc360f970a41c7fca4a..6b856128c2e84dca74f301699c5b7627440a0aab 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -22,14 +22,25 @@ module API
       # Example Request:
       #   GET /projects
       get do
-        @projects = current_user.authorized_projects
-        @projects = filter_projects(@projects)
-        @projects = paginate @projects
-        if params[:simple]
-          present @projects, with: Entities::BasicProjectDetails, user: current_user
-        else
-          present @projects, with: Entities::ProjectWithAccess, user: current_user
-        end
+        projects = current_user.authorized_projects
+        projects = filter_projects(projects)
+        projects = paginate projects
+        entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+        present projects, with: entity, user: current_user
+      end
+
+      # Get a list of visible projects for authenticated user
+      #
+      # Example Request:
+      #   GET /projects/visible
+      get '/visible' do
+        projects = ProjectsFinder.new.execute(current_user)
+        projects = filter_projects(projects)
+        projects = paginate projects
+        entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+        present projects, with: entity, user: current_user
       end
 
       # Get an owned projects list for authenticated user
@@ -37,10 +48,10 @@ module API
       # Example Request:
       #   GET /projects/owned
       get '/owned' do
-        @projects = current_user.owned_projects
-        @projects = filter_projects(@projects)
-        @projects = paginate @projects
-        present @projects, with: Entities::ProjectWithAccess, user: current_user
+        projects = current_user.owned_projects
+        projects = filter_projects(projects)
+        projects = paginate projects
+        present projects, with: Entities::ProjectWithAccess, user: current_user
       end
 
       # Gets starred project for the authenticated user
@@ -48,10 +59,10 @@ module API
       # Example Request:
       #   GET /projects/starred
       get '/starred' do
-        @projects = current_user.viewable_starred_projects
-        @projects = filter_projects(@projects)
-        @projects = paginate @projects
-        present @projects, with: Entities::Project
+        projects = current_user.viewable_starred_projects
+        projects = filter_projects(projects)
+        projects = paginate projects
+        present projects, with: Entities::Project, user: current_user
       end
 
       # Get all projects for admin user
@@ -60,10 +71,10 @@ module API
       #   GET /projects/all
       get '/all' do
         authenticated_as_admin!
-        @projects = Project.all
-        @projects = filter_projects(@projects)
-        @projects = paginate @projects
-        present @projects, with: Entities::ProjectWithAccess, user: current_user
+        projects = Project.all
+        projects = filter_projects(projects)
+        projects = paginate projects
+        present projects, with: Entities::ProjectWithAccess, user: current_user
       end
 
       # Get a single project
@@ -91,8 +102,8 @@ module API
       # Create new project
       #
       # Parameters:
-      #   name (required) - name for new project
-      #   description (optional) - short project description
+      #   name (required)                   - name for new project
+      #   description (optional)            - short project description
       #   issues_enabled (optional)
       #   merge_requests_enabled (optional)
       #   builds_enabled (optional)
@@ -100,30 +111,36 @@ module API
       #   snippets_enabled (optional)
       #   container_registry_enabled (optional)
       #   shared_runners_enabled (optional)
-      #   namespace_id (optional) - defaults to user namespace
-      #   public (optional) - if true same as setting visibility_level = 20
-      #   visibility_level (optional) - 0 by default
+      #   namespace_id (optional)           - defaults to user namespace
+      #   public (optional)                 - if true same as setting visibility_level = 20
+      #   visibility_level (optional)       - 0 by default
       #   import_url (optional)
       #   public_builds (optional)
+      #   lfs_enabled (optional)
+      #   request_access_enabled (optional) - Allow users to request member access
       # Example Request
       #   POST /projects
       post do
         required_attributes! [:name]
-        attrs = attributes_for_keys [:name,
-                                     :path,
+        attrs = attributes_for_keys [:builds_enabled,
+                                     :container_registry_enabled,
                                      :description,
+                                     :import_url,
                                      :issues_enabled,
+                                     :lfs_enabled,
                                      :merge_requests_enabled,
-                                     :builds_enabled,
-                                     :wiki_enabled,
-                                     :snippets_enabled,
-                                     :container_registry_enabled,
-                                     :shared_runners_enabled,
+                                     :name,
                                      :namespace_id,
+                                     :only_allow_merge_if_build_succeeds,
+                                     :path,
                                      :public,
+                                     :public_builds,
+                                     :request_access_enabled,
+                                     :shared_runners_enabled,
+                                     :snippets_enabled,
                                      :visibility_level,
-                                     :import_url,
-                                     :public_builds]
+                                     :wiki_enabled,
+                                     :only_allow_merge_if_all_discussions_are_resolved]
         attrs = map_public_to_visibility_level(attrs)
         @project = ::Projects::CreateService.new(current_user, attrs).execute
         if @project.saved?
@@ -140,10 +157,10 @@ module API
       # Create new project for a specified user.  Only available to admin users.
       #
       # Parameters:
-      #   user_id (required) - The ID of a user
-      #   name (required) - name for new project
-      #   description (optional) - short project description
-      #   default_branch (optional) - 'master' by default
+      #   user_id (required)                - The ID of a user
+      #   name (required)                   - name for new project
+      #   description (optional)            - short project description
+      #   default_branch (optional)         - 'master' by default
       #   issues_enabled (optional)
       #   merge_requests_enabled (optional)
       #   builds_enabled (optional)
@@ -151,28 +168,34 @@ module API
       #   snippets_enabled (optional)
       #   container_registry_enabled (optional)
       #   shared_runners_enabled (optional)
-      #   public (optional) - if true same as setting visibility_level = 20
+      #   public (optional)                 - if true same as setting visibility_level = 20
       #   visibility_level (optional)
       #   import_url (optional)
       #   public_builds (optional)
+      #   lfs_enabled (optional)
+      #   request_access_enabled (optional) - Allow users to request member access
       # Example Request
       #   POST /projects/user/:user_id
       post "user/:user_id" do
         authenticated_as_admin!
         user = User.find(params[:user_id])
-        attrs = attributes_for_keys [:name,
-                                     :description,
+        attrs = attributes_for_keys [:builds_enabled,
                                      :default_branch,
+                                     :description,
+                                     :import_url,
                                      :issues_enabled,
+                                     :lfs_enabled,
                                      :merge_requests_enabled,
-                                     :builds_enabled,
-                                     :wiki_enabled,
-                                     :snippets_enabled,
-                                     :shared_runners_enabled,
+                                     :name,
+                                     :only_allow_merge_if_build_succeeds,
                                      :public,
+                                     :public_builds,
+                                     :request_access_enabled,
+                                     :shared_runners_enabled,
+                                     :snippets_enabled,
                                      :visibility_level,
-                                     :import_url,
-                                     :public_builds]
+                                     :wiki_enabled,
+                                     :only_allow_merge_if_all_discussions_are_resolved]
         attrs = map_public_to_visibility_level(attrs)
         @project = ::Projects::CreateService.new(user, attrs).execute
         if @project.saved?
@@ -183,16 +206,32 @@ module API
         end
       end
 
-      # Fork new project for the current user.
+      # Fork new project for the current user or provided namespace.
       #
       # Parameters:
       #   id (required) - The ID of a project
+      #   namespace (optional) - The ID or name of the namespace that the project will be forked into.
       # Example Request
       #   POST /projects/fork/:id
       post 'fork/:id' do
+        attrs = {}
+        namespace_id = params[:namespace]
+
+        if namespace_id.present?
+          namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
+
+          unless namespace && can?(current_user, :create_projects, namespace)
+            not_found!('Target Namespace')
+          end
+
+          attrs[:namespace] = namespace
+        end
+
         @forked_project =
           ::Projects::ForkService.new(user_project,
-                                      current_user).execute
+                                      current_user,
+                                      attrs).execute
+
         if @forked_project.errors.any?
           conflict!(@forked_project.errors.messages)
         else
@@ -218,23 +257,28 @@ module API
       #   public (optional) - if true same as setting visibility_level = 20
       #   visibility_level (optional) - visibility level of a project
       #   public_builds (optional)
+      #   lfs_enabled (optional)
       # Example Request
       #   PUT /projects/:id
       put ':id' do
-        attrs = attributes_for_keys [:name,
-                                     :path,
-                                     :description,
+        attrs = attributes_for_keys [:builds_enabled,
+                                     :container_registry_enabled,
                                      :default_branch,
+                                     :description,
                                      :issues_enabled,
+                                     :lfs_enabled,
                                      :merge_requests_enabled,
-                                     :builds_enabled,
-                                     :wiki_enabled,
-                                     :snippets_enabled,
-                                     :container_registry_enabled,
-                                     :shared_runners_enabled,
+                                     :name,
+                                     :only_allow_merge_if_build_succeeds,
+                                     :path,
                                      :public,
+                                     :public_builds,
+                                     :request_access_enabled,
+                                     :shared_runners_enabled,
+                                     :snippets_enabled,
                                      :visibility_level,
-                                     :public_builds]
+                                     :wiki_enabled,
+                                     :only_allow_merge_if_all_discussions_are_resolved]
         attrs = map_public_to_visibility_level(attrs)
         authorize_admin_project
         authorize! :rename_project, user_project if attrs[:name].present?
@@ -363,23 +407,30 @@ module API
       # Share project with group
       #
       # Parameters:
-      #   id (required) - The ID of a project
-      #   group_id (required) - The ID of a group
+      #   id (required)           - The ID of a project
+      #   group_id (required)     - The ID of a group
       #   group_access (required) - Level of permissions for sharing
+      #   expires_at (optional)   - Share expiration date
       #
       # Example Request:
       #   POST /projects/:id/share
       post ":id/share" do
         authorize! :admin_project, user_project
         required_attributes! [:group_id, :group_access]
+        attrs = attributes_for_keys [:group_id, :group_access, :expires_at]
+
+        group = Group.find_by_id(attrs[:group_id])
+
+        unless group && can?(current_user, :read_group, group)
+          not_found!('Group')
+        end
 
         unless user_project.allowed_to_share_with_group?
           return render_api_error!("The project sharing with group is disabled", 400)
         end
 
-        link = user_project.project_group_links.new
-        link.group_id = params[:group_id]
-        link.group_access = params[:group_access]
+        link = user_project.project_group_links.new(attrs)
+
         if link.save
           present link, with: Entities::ProjectGroupLink
         else
@@ -405,18 +456,9 @@ module API
       # Example Request:
       #   GET /projects/search/:query
       get "/search/:query" do
-        ids = current_user.authorized_projects.map(&:id)
-        visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ]
-        projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%")
-        sort = params[:sort] == 'desc' ? 'desc' : 'asc'
-
-        projects = case params["order_by"]
-                   when 'id' then projects.order("id #{sort}")
-                   when 'name' then projects.order("name #{sort}")
-                   when 'created_at' then projects.order("created_at #{sort}")
-                   when 'last_activity_at' then projects.order("last_activity_at #{sort}")
-                   else projects
-                   end
+        search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
+        projects = search_service.objects('projects', params[:page])
+        projects = projects.reorder(project_order_by => project_sort)
 
         present paginate(projects), with: Entities::Project
       end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ecc8f2fc5a2307e9e84b103600e246ee56106bd8..84c19c432b01165fb6206047777e07970d8c3543 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -1,34 +1,39 @@
 module API
-  # Runners API
   class Runners < Grape::API
     before { authenticate! }
 
     resource :runners do
-      # Get runners available for user
-      #
-      # Example Request:
-      #   GET /runners
+      desc 'Get runners available for user' do
+        success Entities::Runner
+      end
+      params do
+        optional :scope, type: String, values: %w[active paused online],
+                         desc: 'The scope of specific runners to show'
+      end
       get do
         runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
         present paginate(runners), with: Entities::Runner
       end
 
-      # Get all runners - shared and specific
-      #
-      # Example Request:
-      #   GET /runners/all
+      desc 'Get all runners - shared and specific' do
+        success Entities::Runner
+      end
+      params do
+        optional :scope, type: String, values: %w[active paused online specific shared],
+                         desc: 'The scope of specific runners to show'
+      end
       get 'all' do
         authenticated_as_admin!
         runners = filter_runners(Ci::Runner.all, params[:scope])
         present paginate(runners), with: Entities::Runner
       end
 
-      # Get runner's details
-      #
-      # Parameters:
-      #   id (required) - The ID of ther runner
-      # Example Request:
-      #   GET /runners/:id
+      desc "Get runner's details" do
+        success Entities::RunnerDetails
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of the runner'
+      end
       get ':id' do
         runner = get_runner(params[:id])
         authenticate_show_runner!(runner)
@@ -36,33 +41,37 @@ module API
         present runner, with: Entities::RunnerDetails, current_user: current_user
       end
 
-      # Update runner's details
-      #
-      # Parameters:
-      #   id (required) - The ID of ther runner
-      #   description (optional) - Runner's description
-      #   active (optional) - Runner's status
-      #   tag_list (optional) - Array of tags for runner
-      # Example Request:
-      #   PUT /runners/:id
+      desc "Update runner's details" do
+        success Entities::RunnerDetails
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of the runner'
+        optional :description, type: String, desc: 'The description of the runner'
+        optional :active, type: Boolean, desc: 'The state of a runner'
+        optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
+        optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
+        optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
+        at_least_one_of :description, :active, :tag_list, :run_untagged, :locked
+      end
       put ':id' do
-        runner = get_runner(params[:id])
+        runner = get_runner(params.delete(:id))
         authenticate_update_runner!(runner)
 
-        attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
-        if runner.update(attrs)
+        runner_params = declared(params, include_missing: false)
+
+        if runner.update(runner_params)
           present runner, with: Entities::RunnerDetails, current_user: current_user
         else
           render_validation_error!(runner)
         end
       end
 
-      # Remove runner
-      #
-      # Parameters:
-      #   id (required) - The ID of ther runner
-      # Example Request:
-      #   DELETE /runners/:id
+      desc 'Remove a runner' do
+        success Entities::Runner
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of the runner'
+      end
       delete ':id' do
         runner = get_runner(params[:id])
         authenticate_delete_runner!(runner)
@@ -72,28 +81,31 @@ module API
       end
     end
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
       before { authorize_admin_project }
 
-      # Get runners available for project
-      #
-      # Example Request:
-      #   GET /projects/:id/runners
+      desc 'Get runners available for project' do
+        success Entities::Runner
+      end
+      params do
+        optional :scope, type: String, values: %w[active paused online specific shared],
+                         desc: 'The scope of specific runners to show'
+      end
       get ':id/runners' do
         runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
         present paginate(runners), with: Entities::Runner
       end
 
-      # Enable runner for project
-      #
-      # Parameters:
-      #   id (required) - The ID of the project
-      #   runner_id (required) - The ID of the runner
-      # Example Request:
-      #   POST /projects/:id/runners/:runner_id
+      desc 'Enable a runner for a project' do
+        success Entities::Runner
+      end
+      params do
+        requires :runner_id, type: Integer, desc: 'The ID of the runner'
+      end
       post ':id/runners' do
-        required_attributes! [:runner_id]
-
         runner = get_runner(params[:runner_id])
         authenticate_enable_runner!(runner)
 
@@ -106,13 +118,12 @@ module API
         end
       end
 
-      # Disable project's runner
-      #
-      # Parameters:
-      #   id (required) - The ID of the project
-      #   runner_id (required) - The ID of the runner
-      # Example Request:
-      #   DELETE /projects/:id/runners/:runner_id
+      desc "Disable project's runner" do
+        success 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
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 56c202f129435eedad105f015b8a845afb4f5994..d09400b81f5f21b92d93fedb59501a4fef360af9 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -1,19 +1,19 @@
 module API
-  # Users API
   class Session < Grape::API
-    # Login to get token
-    #
-    # Parameters:
-    #   login (*required) - user login
-    #   email (*required) - user email
-    #   password (required) - user password
-    #
-    # Example Request:
-    #  POST /session
+    desc 'Login to get token' do
+      success Entities::UserLogin
+    end
+    params do
+      optional :login, type: String, desc: 'The username'
+      optional :email, type: String, desc: 'The email of the user'
+      requires :password, type: String, desc: 'The password of the user'
+      at_least_one_of :login, :email
+    end
     post "/session" do
       user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
 
       return unauthorized! unless user
+      return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
       present user, with: Entities::UserLogin
     end
   end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c885fcd7ea34abe2d8417b7e8fc0eb62e27a8545..c4cb1c7924ab534fdd6b16f49a7ff2b794f13fdd 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -17,12 +17,12 @@ module API
       present current_settings, with: Entities::ApplicationSetting
     end
 
-    # Modify applicaiton settings
+    # Modify application settings
     #
     # Example Request:
     #   PUT /application/settings
     put "application/settings" do
-      attributes = current_settings.attributes.keys - ["id"]
+      attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"]
       attrs = attributes_for_keys(attributes)
 
       if current_settings.update_attributes(attrs)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 22b8f90dc5c6b95fc5fc0bdf05c2f744df19f7d9..b6bfff9f20f2d2387ac96387a48dca8c167f539a 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -7,38 +7,41 @@ module API
     end
 
     resource :hooks do
-      # Get the list of system hooks
-      #
-      # Example Request:
-      #   GET /hooks
+      desc 'Get the list of system hooks' do
+        success Entities::Hook
+      end
       get do
-        @hooks = SystemHook.all
-        present @hooks, with: Entities::Hook
+        hooks = SystemHook.all
+
+        present hooks, with: Entities::Hook
       end
 
-      # Create new system hook
-      #
-      # Parameters:
-      #   url (required) - url for system hook
-      # Example Request
-      #   POST /hooks
+      desc 'Create a new system hook' do
+        success Entities::Hook
+      end
+      params do
+        requires :url, type: String, desc: "The URL to send the request to"
+        optional :token, type: String, desc: 'The token used to validate payloads'
+        optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+        optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+        optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+      end
       post do
-        attrs = attributes_for_keys [:url]
-        required_attributes! [:url]
-        @hook = SystemHook.new attrs
-        if @hook.save
-          present @hook, with: Entities::Hook
+        hook = SystemHook.new declared(params, include_missing: false).to_h
+
+        if hook.save
+          present hook, with: Entities::Hook
         else
-          not_found!
+          render_validation_error!(hook)
         end
       end
 
-      # Test a hook
-      #
-      # Example Request
-      #   GET /hooks/:id
+      desc 'Test a hook'
+      params do
+        requires :id, type: Integer, desc: 'The ID of the system hook'
+      end
       get ":id" do
-        @hook = SystemHook.find(params[:id])
+        hook = SystemHook.find(params[:id])
         data = {
           event_name: "project_create",
           name: "Ruby",
@@ -47,23 +50,21 @@ module API
           owner_name: "Someone",
           owner_email: "example@gitlabhq.com"
         }
-        @hook.execute(data, 'system_hooks')
+        hook.execute(data, 'system_hooks')
         data
       end
 
-      # Delete a hook. This is an idempotent function.
-      #
-      # Parameters:
-      #   id (required) - ID of the hook
-      # Example Request:
-      #   DELETE /hooks/:id
+      desc 'Delete a hook' do
+        success Entities::Hook
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of the system hook'
+      end
       delete ":id" do
-        begin
-          @hook = SystemHook.find(params[:id])
-          @hook.destroy
-        rescue
-          # SystemHook raises an Error if no hook with id found
-        end
+        hook = SystemHook.find_by(id: params[:id])
+        not_found!('System hook') unless hook
+
+        present hook.destroy, with: Entities::Hook
       end
     end
   end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 7b675e05fbb7be686d47945d991edc5c3546280d..bf2a199ce21d62cafb0e8af0e4a8e6c17a34a8e1 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -4,25 +4,24 @@ module API
     before { authenticate! }
     before { authorize! :download_code, user_project }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Get a project repository tags
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      # Example Request:
-      #   GET /projects/:id/repository/tags
+      desc 'Get a project repository tags' do
+        success Entities::RepoTag
+      end
       get ":id/repository/tags" do
         present user_project.repository.tags.sort_by(&:name).reverse,
                 with: Entities::RepoTag, project: user_project
       end
 
-      # Get a single repository tag
-      #
-      # Parameters:
-      #   id (required)       - The ID of a project
-      #   tag_name (required) - The name of the tag
-      # Example Request:
-      #   GET /projects/:id/repository/tags/:tag_name
+      desc 'Get a single repository tag' do
+        success Entities::RepoTag
+      end
+      params do
+        requires :tag_name, type: String, desc: 'The name of the tag'
+      end
       get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
         tag = user_project.repository.find_tag(params[:tag_name])
         not_found!('Tag') unless tag
@@ -30,20 +29,21 @@ module API
         present tag, with: Entities::RepoTag, project: user_project
       end
 
-      # Create tag
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   tag_name (required) - The name of the tag
-      #   ref (required) - Create tag from commit sha or branch
-      #   message (optional) - Specifying a message creates an annotated tag.
-      # Example Request:
-      #   POST /projects/:id/repository/tags
+      desc 'Create a new repository tag' do
+        success Entities::RepoTag
+      end
+      params do
+        requires :tag_name,            type: String, desc: 'The name of the tag'
+        requires :ref,                 type: String, desc: 'The commit sha or branch name'
+        optional :message,             type: String, desc: 'Specifying a message creates an annotated tag'
+        optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database'
+      end
       post ':id/repository/tags' do
         authorize_push_project
-        message = params[:message] || nil
+        create_params = declared(params)
+
         result = CreateTagService.new(user_project, current_user).
-          execute(params[:tag_name], params[:ref], message, params[:release_description])
+          execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
 
         if result[:status] == :success
           present result[:tag],
@@ -54,15 +54,13 @@ module API
         end
       end
 
-      # Delete tag
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   tag_name (required) - The name of the tag
-      # Example Request:
-      #   DELETE /projects/:id/repository/tags/:tag
+      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 = DeleteTagService.new(user_project, current_user).
           execute(params[:tag_name])
 
@@ -75,17 +73,16 @@ module API
         end
       end
 
-      # Add release notes to tag
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   tag_name (required) - The name of the tag
-      #   description (required) - Release notes with markdown support
-      # Example Request:
-      #   POST /projects/:id/repository/tags/:tag_name/release
+      desc 'Add a release note to a tag' do
+        success Entities::Release
+      end
+      params do
+        requires :tag_name,    type: String, desc: 'The name of the tag'
+        requires :description, type: String, desc: 'Release notes with markdown support'
+      end
       post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
         authorize_push_project
-        required_attributes! [:description]
+
         result = CreateReleaseService.new(user_project, current_user).
           execute(params[:tag_name], params[:description])
 
@@ -96,17 +93,16 @@ module API
         end
       end
 
-      # Updates a release notes of a tag
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   tag_name (required) - The name of the tag
-      #   description (required) - Release notes with markdown support
-      # Example Request:
-      #   PUT /projects/:id/repository/tags/:tag_name/release
+      desc "Update a tag's release note" do
+        success Entities::Release
+      end
+      params do
+        requires :tag_name,    type: String, desc: 'The name of the tag'
+        requires :description, type: String, desc: 'Release notes with markdown support'
+      end
       put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
         authorize_push_project
-        required_attributes! [:description]
+
         result = UpdateReleaseService.new(user_project, current_user).
           execute(params[:tag_name], params[:description])
 
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index b9e718147e10d1e9285c96e0ed582b0f1a023357..8a53d9c0095d5355224bb19e40ce174b734b49f4 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,39 +1,115 @@
 module API
   class Templates < Grape::API
     GLOBAL_TEMPLATE_TYPES = {
-      gitignores:     Gitlab::Template::GitignoreTemplate,
-      gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
+      gitignores: {
+        klass: Gitlab::Template::GitignoreTemplate,
+        gitlab_version: 8.8
+      },
+      gitlab_ci_ymls: {
+        klass: Gitlab::Template::GitlabCiYmlTemplate,
+        gitlab_version: 8.9
+      }
     }.freeze
+    PROJECT_TEMPLATE_REGEX =
+      /[\<\{\[]
+        (project|description|
+        one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+      [\>\}\]]/xi.freeze
+    YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+    FULLNAME_TEMPLATE_REGEX =
+      /[\<\{\[]
+        (fullname|name\sof\s(author|copyright\sowner))
+      [\>\}\]]/xi.freeze
+    DEPRECATION_MESSAGE = ' This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
 
     helpers do
+      def parsed_license_template
+        # We create a fresh Licensee::License object since we'll modify its
+        # content in place below.
+        template = Licensee::License.new(params[:name])
+
+        template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+        template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+        fullname = params[:fullname].presence || current_user.try(:name)
+        template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+        template
+      end
+
       def render_response(template_type, template)
         not_found!(template_type.to_s.singularize) unless template
         present template, with: Entities::Template
       end
     end
 
-    GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
-      # Get the list of the available template
-      #
-      # Example Request:
-      #   GET /gitignores
-      #   GET /gitlab_ci_ymls
-      get template_type.to_s do
-        present klass.all, with: Entities::TemplatesList
-      end
-
-      # Get the text for a specific template present in local filesystem
-      #
-      # Parameters:
-      #   name (required) - The name of a template
-      #
-      # Example Request:
-      #   GET /gitignores/Elixir
-      #   GET /gitlab_ci_ymls/Ruby
-      get "#{template_type}/:name" do
-        required_attributes! [:name]
-        new_template = klass.find(params[:name])
-        render_response(template_type, new_template)
+    { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
+      desc 'Get the list of the available license template' do
+        detailed_desc = 'This feature was introduced in GitLab 8.7.'
+        detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+        detail detailed_desc
+        success Entities::RepoLicense
+      end
+      params do
+        optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+      end 
+      get route do
+        options = {
+          featured: declared(params).popular.present? ? true : nil
+        }
+        present Licensee::License.all(options), with: Entities::RepoLicense
+      end
+    end
+
+    { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
+      desc 'Get the text for a specific license' do
+        detailed_desc = 'This feature was introduced in GitLab 8.7.'
+        detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+        detail detailed_desc
+        success Entities::RepoLicense
+      end
+      params do
+        requires :name, type: String, desc: 'The name of the template'
+      end 
+      get route, requirements: { name: /[\w\.-]+/ } do
+        not_found!('License') unless Licensee::License.find(declared(params).name)
+
+        template = parsed_license_template
+
+        present template, with: Entities::RepoLicense
+      end
+    end
+      
+    GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
+      klass = properties[:klass]
+      gitlab_version = properties[:gitlab_version]
+
+      { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
+        desc 'Get the list of the available template' do
+          detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+          detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+          detail detailed_desc
+          success Entities::TemplatesList
+        end
+        get route do
+          present klass.all, with: Entities::TemplatesList
+        end
+      end
+
+      { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
+        desc 'Get the text for a specific template present in local filesystem' do
+          detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+          detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+          detail detailed_desc
+          success Entities::Template
+        end
+        params do
+          requires :name, type: String, desc: 'The name of the template'
+        end 
+        get route do
+          new_template = klass.find(declared(params).name)
+
+          render_response(template_type, new_template)
+        end
       end
     end
   end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 19df13d8aacad3eca25872bda4e7ee8877d3de2e..832b04a3bb12f4375dd41e93e73ad1cfd4de5b67 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -8,18 +8,19 @@ module API
       'issues' => ->(id) { find_project_issue(id) }
     }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
       ISSUABLE_TYPES.each do |type, finder|
         type_id_str = "#{type.singularize}_id".to_sym
 
-        # Create a todo on an issuable
-        #
-        # Parameters:
-        #   id (required) - The ID of a project
-        #   issuable_id (required) - The ID of an issuable
-        # Example Request:
-        #   POST /projects/:id/issues/:issuable_id/todo
-        #   POST /projects/:id/merge_requests/:issuable_id/todo
+        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'
+        end
         post ":id/#{type}/:#{type_id_str}/todo" do
           issuable = instance_exec(params[type_id_str], &finder)
           todo = TodoService.new.mark_todo(issuable, current_user).first
@@ -40,25 +41,21 @@ module API
         end
       end
 
-      # Get a todo list
-      #
-      # Example Request:
-      #  GET /todos
-      #
+      desc 'Get a todo list' do
+        success Entities::Todo
+      end
       get do
         todos = find_todos
 
         present paginate(todos), with: Entities::Todo, current_user: current_user
       end
 
-      # Mark a todo as done
-      #
-      # Parameters:
-      #   id: (required) - The ID of the todo being marked as done
-      #
-      # Example Request:
-      #  DELETE /todos/:id
-      #
+      desc 'Mark a todo as done' do
+        success Entities::Todo
+      end
+      params do
+        requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
+      end
       delete ':id' do
         todo = current_user.todos.find(params[:id])
         TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,11 +63,7 @@ module API
         present todo.reload, with: Entities::Todo, current_user: current_user
       end
 
-      # Mark all todos as done
-      #
-      # Example Request:
-      #  DELETE /todos
-      #
+      desc 'Mark all todos as done'
       delete do
         todos = find_todos
         TodoService.new.mark_todos_as_done(todos, current_user)
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index d1d07394e92847bae0e4c7ee8efd4f4914e8ee4a..9a4f1cd342f2208749fd50bd6f613b6c8c00a7f8 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -1,19 +1,18 @@
 module API
-  # Triggers API
   class Triggers < Grape::API
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects do
-      # Trigger a GitLab project build
-      #
-      # Parameters:
-      #   id (required) - The ID of a CI project
-      #   ref (required) - The name of project's branch or tag
-      #   token (required) - The uniq token of trigger
-      #   variables (optional) - The list of variables to be injected into build
-      # Example Request:
-      #   POST /projects/:id/trigger/builds
+      desc 'Trigger a GitLab project build' do
+        success 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/trigger/builds" do
-        required_attributes! [:ref, :token]
-
         project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
         trigger = Ci::Trigger.find_by_token(params[:token].to_s)
         not_found! unless project && trigger
@@ -22,10 +21,6 @@ module API
         # validate variables
         variables = params[:variables]
         if variables
-          unless variables.is_a?(Hash)
-            render_api_error!('variables needs to be a hash', 400)
-          end
-
           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
@@ -44,31 +39,24 @@ module API
         end
       end
 
-      # Get triggers list
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   page (optional) - The page number for pagination
-      #   per_page (optional) - The value of items per page to show
-      # Example Request:
-      #   GET /projects/:id/triggers
+      desc 'Get triggers list' do
+        success Entities::Trigger
+      end
       get ':id/triggers' do
         authenticate!
         authorize! :admin_build, user_project
 
         triggers = user_project.triggers.includes(:trigger_requests)
-        triggers = paginate(triggers)
 
-        present triggers, with: Entities::Trigger
+        present paginate(triggers), with: Entities::Trigger
       end
 
-      # Get specific trigger of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   token (required) - The `token` of a trigger
-      # Example Request:
-      #   GET /projects/:id/triggers/:token
+      desc 'Get specific trigger of a project' do
+        success 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
@@ -79,12 +67,9 @@ module API
         present trigger, with: Entities::Trigger
       end
 
-      # Create trigger
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      # Example Request:
-      #   POST /projects/:id/triggers
+      desc 'Create a trigger' do
+        success Entities::Trigger
+      end
       post ':id/triggers' do
         authenticate!
         authorize! :admin_build, user_project
@@ -94,13 +79,12 @@ module API
         present trigger, with: Entities::Trigger
       end
 
-      # Delete trigger
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   token (required) - The `token` of a trigger
-      # Example Request:
-      #   DELETE /projects/:id/triggers/:token
+      desc 'Delete a trigger' do
+        success 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
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 8a376d3c2a322051bb5aa8e70d913f41cda27cf5..298c401a816348ac5285e8af434a0f180145bd38 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -10,6 +10,9 @@ module API
       #  GET /users
       #  GET /users?search=Admin
       #  GET /users?username=root
+      #  GET /users?active=true
+      #  GET /users?external=true
+      #  GET /users?blocked=true
       get do
         unless can?(current_user, :read_users_list, nil)
           render_api_error!("Not authorized.", 403)
@@ -19,8 +22,10 @@ module API
           @users = User.where(username: params[:username])
         else
           @users = User.all
-          @users = @users.active if params[:active].present?
+          @users = @users.active if to_boolean(params[:active])
           @users = @users.search(params[:search]) if params[:search].present?
+          @users = @users.blocked if to_boolean(params[:blocked])
+          @users = @users.external if to_boolean(params[:external]) && current_user.is_admin?
           @users = paginate @users
         end
 
@@ -60,6 +65,7 @@ module API
       #   linkedin                          - Linkedin
       #   twitter                           - Twitter account
       #   website_url                       - Website url
+      #   organization                      - Organization
       #   projects_limit                    - Number of projects user can create
       #   extern_uid                        - External authentication provider UID
       #   provider                          - External provider
@@ -74,7 +80,7 @@ module API
       post do
         authenticated_as_admin!
         required_attributes! [:email, :password, :name, :username]
-        attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
+        attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
         admin = attrs.delete(:admin)
         confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
         user = User.build_user(attrs)
@@ -111,6 +117,7 @@ module API
       #   linkedin                          - Linkedin
       #   twitter                           - Twitter account
       #   website_url                       - Website url
+      #   organization                      - Organization
       #   projects_limit                    - Limit projects each user can create
       #   bio                               - Bio
       #   location                          - Location of the user
@@ -122,7 +129,7 @@ module API
       put ":id" do
         authenticated_as_admin!
 
-        attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
+        attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
         user = User.find(params[:id])
         not_found!('User') unless user
 
@@ -319,6 +326,26 @@ module API
           user.activate
         end
       end
+
+      desc 'Get contribution events of a specified user' do
+        detail 'This feature was introduced in GitLab 8.13.'
+        success Entities::Event
+      end
+      params do
+        requires :id, type: String, desc: 'The user ID'
+      end
+      get ':id/events' do
+        user = User.find_by(id: declared(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: Entities::Event
+      end
     end
 
     resource :user do
@@ -327,7 +354,7 @@ module API
       # Example Request:
       #   GET /user
       get do
-        present @current_user, with: Entities::UserLogin
+        present @current_user, with: Entities::UserFull
       end
 
       # Get currently authenticated user's keys
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f6495071a11b96d0f64b81f3874fea59f79533c7..b9fb3c21dbb808c8a94fa121a2c22b95931e8b74 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -4,27 +4,29 @@ module API
     before { authenticate! }
     before { authorize! :admin_build, user_project }
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+
     resource :projects do
-      # Get project variables
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   page (optional) - The page number for pagination
-      #   per_page (optional) - The value of items per page to show
-      # Example Request:
-      #   GET /projects/:id/variables
+      desc 'Get project variables' do
+        success Entities::Variable
+      end
+      params do
+        optional :page, type: Integer, desc: 'The page number for pagination'
+        optional :per_page, type: Integer, desc: 'The value of items per page to show'
+      end
       get ':id/variables' do
         variables = user_project.variables
         present paginate(variables), with: Entities::Variable
       end
 
-      # Get specific variable of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   key (required) - The `key` of variable
-      # Example Request:
-      #   GET /projects/:id/variables/:key
+      desc 'Get a specific variable from a project' do
+        success Entities::Variable
+      end
+      params do
+        requires :key, type: String, desc: 'The key of the variable'
+      end
       get ':id/variables/:key' do
         key = params[:key]
         variable = user_project.variables.find_by(key: key.to_s)
@@ -34,18 +36,15 @@ module API
         present variable, with: Entities::Variable
       end
 
-      # Create a new variable in project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   key (required) - The key of variable
-      #   value (required) - The value of variable
-      # Example Request:
-      #   POST /projects/:id/variables
+      desc 'Create a new variable in a project' do
+        success Entities::Variable
+      end
+      params do
+        requires :key, type: String, desc: 'The key of the variable'
+        requires :value, type: String, desc: 'The value of the variable'
+      end
       post ':id/variables' do
-        required_attributes! [:key, :value]
-
-        variable = user_project.variables.create(key: params[:key], value: params[:value])
+        variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
 
         if variable.valid?
           present variable, with: Entities::Variable
@@ -54,41 +53,37 @@ module API
         end
       end
 
-      # Update existing variable of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   key (optional) - The `key` of variable
-      #   value (optional) - New value for `value` field of variable
-      # Example Request:
-      #   PUT /projects/:id/variables/:key
+      desc 'Update an existing variable from a project' do
+        success Entities::Variable
+      end
+      params do
+        optional :key, type: String, desc: 'The key of the variable'
+        optional :value, type: String, desc: 'The value of the variable'
+      end
       put ':id/variables/:key' do
-        variable = user_project.variables.find_by(key: params[:key].to_s)
+        variable = user_project.variables.find_by(key: params[:key])
 
         return not_found!('Variable') unless variable
 
-        attrs = attributes_for_keys [:value]
-        if variable.update(attrs)
+        if variable.update(value: params[:value])
           present variable, with: Entities::Variable
         else
           render_validation_error!(variable)
         end
       end
 
-      # Delete existing variable of a project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   key (required) - The ID of a variable
-      # Example Request:
-      #   DELETE /projects/:id/variables/:key
+      desc 'Delete an existing variable from a project' do
+        success 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].to_s)
+        variable = user_project.variables.find_by(key: params[:key])
 
         return not_found!('Variable') unless variable
-        variable.destroy
 
-        present variable, with: Entities::Variable
+        present variable.destroy, with: Entities::Variable
       end
     end
   end
diff --git a/lib/api/version.rb b/lib/api/version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ba576bd8286cc8b9e61b54dd4b7b7b49eb52c10
--- /dev/null
+++ b/lib/api/version.rb
@@ -0,0 +1,12 @@
+module API
+  class Version < Grape::API
+    before { authenticate! }
+
+    desc 'Get the version information of the GitLab instance.' do
+      detail 'This feature was introduced in GitLab 8.13.'
+    end
+    get '/version' do
+      { version: Gitlab::VERSION, revision: Gitlab::REVISION }
+    end
+  end
+end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index f117fc3d37def0815360c09e4221dbadea2a9f11..d746070913d3008b1082b764e61c781c915b084f 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,11 +2,14 @@ require 'yaml'
 
 module Backup
   class Repository
+
     def dump
       prepare
 
       Project.find_each(batch_size: 1000) do |project|
         $progress.print " * #{project.path_with_namespace} ... "
+        path_to_project_repo = path_to_repo(project)
+        path_to_project_bundle = path_to_bundle(project)
 
         # Create namespace dir if missing
         FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
@@ -14,8 +17,22 @@ module Backup
         if project.empty_repo?
           $progress.puts "[SKIPPED]".color(:cyan)
         else
-          cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
+          in_path(path_to_project_repo) do |dir|
+            FileUtils.mkdir_p(path_to_tars(project))
+            cmd = %W(tar -cf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+            output, status = Gitlab::Popen.popen(cmd)
+
+            unless status.zero?
+              puts "[FAILED]".color(:red)
+              puts "failed: #{cmd.join(' ')}"
+              puts output
+              abort 'Backup failed'
+            end
+          end
+
+          cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_project_repo} bundle create #{path_to_project_bundle} --all)
           output, status = Gitlab::Popen.popen(cmd)
+
           if status.zero?
             $progress.puts "[DONE]".color(:green)
           else
@@ -27,19 +44,22 @@ module Backup
         end
 
         wiki = ProjectWiki.new(project)
+        path_to_wiki_repo = path_to_repo(wiki)
+        path_to_wiki_bundle = path_to_bundle(wiki)
 
-        if File.exist?(path_to_repo(wiki))
+        if File.exist?(path_to_wiki_repo)
           $progress.print " * #{wiki.path_with_namespace} ... "
           if wiki.repository.empty?
             $progress.puts " [SKIPPED]".color(:cyan)
           else
-            cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
+            cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all)
             output, status = Gitlab::Popen.popen(cmd)
             if status.zero?
               $progress.puts " [DONE]".color(:green)
             else
               puts " [FAILED]".color(:red)
               puts "failed: #{cmd.join(' ')}"
+              puts output
               abort 'Backup failed'
             end
           end
@@ -55,45 +75,64 @@ module Backup
         bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
         FileUtils.mv(path, bk_repos_path)
         # This is expected from gitlab:check
-        FileUtils.mkdir_p(path, mode: 2770)
+        FileUtils.mkdir_p(path, mode: 02770)
       end
 
       Project.find_each(batch_size: 1000) do |project|
         $progress.print " * #{project.path_with_namespace} ... "
+        path_to_project_repo = path_to_repo(project)
+        path_to_project_bundle = path_to_bundle(project)
 
         project.ensure_dir_exist
 
-        if File.exist?(path_to_bundle(project))
-          FileUtils.mkdir_p(path_to_repo(project))
-          cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
+        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_repo(project)})
+          cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
         end
 
-        if system(*cmd, silent)
+        output, status = Gitlab::Popen.popen(cmd)
+        if status.zero?
           $progress.puts "[DONE]".color(:green)
         else
           puts "[FAILED]".color(:red)
           puts "failed: #{cmd.join(' ')}"
+          puts output
           abort 'Restore failed'
         end
 
+        in_path(path_to_tars(project)) do |dir|
+          cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+
+          output, status = Gitlab::Popen.popen(cmd)
+          unless status.zero?
+            puts "[FAILED]".color(:red)
+            puts "failed: #{cmd.join(' ')}"
+            puts output
+            abort 'Restore failed'
+          end
+        end
+
         wiki = ProjectWiki.new(project)
+        path_to_wiki_repo = path_to_repo(wiki)
+        path_to_wiki_bundle = path_to_bundle(wiki)
 
-        if File.exist?(path_to_bundle(wiki))
+        if File.exist?(path_to_wiki_bundle)
           $progress.print " * #{wiki.path_with_namespace} ... "
 
           # If a wiki bundle exists, first remove the empty repo
           # that was initialized with ProjectWiki.new() and then
           # try to restore with 'git clone --bare'.
-          FileUtils.rm_rf(path_to_repo(wiki))
-          cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
+          FileUtils.rm_rf(path_to_wiki_repo)
+          cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo})
 
-          if system(*cmd, silent)
+          output, status = Gitlab::Popen.popen(cmd)
+          if status.zero?
             $progress.puts " [DONE]".color(:green)
           else
             puts " [FAILED]".color(:red)
             puts "failed: #{cmd.join(' ')}"
+            puts output
             abort 'Restore failed'
           end
         end
@@ -101,13 +140,15 @@ module Backup
 
       $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
       cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args
-      if system(*cmd)
+
+      output, status = Gitlab::Popen.popen(cmd)
+      if status.zero?
         $progress.puts " [DONE]".color(:green)
       else
         puts " [FAILED]".color(:red)
         puts "failed: #{cmd}"
+        puts output
       end
-
     end
 
     protected
@@ -117,11 +158,30 @@ module Backup
     end
 
     def path_to_bundle(project)
-      File.join(backup_repos_path, project.path_with_namespace + ".bundle")
+      File.join(backup_repos_path, project.path_with_namespace + '.bundle')
+    end
+
+    def path_to_tars(project, dir = nil)
+      path = File.join(backup_repos_path, project.path_with_namespace)
+
+      if dir
+        File.join(path, "#{dir}.tar")
+      else
+        path
+      end
     end
 
     def backup_repos_path
-      File.join(Gitlab.config.backup.path, "repositories")
+      File.join(Gitlab.config.backup.path, 'repositories')
+    end
+
+    def in_path(path)
+      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
     end
 
     def prepare
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 9ebe379f454e04691b212b439984ef0c34174013..35ca234c1ba1d148f39a3b461a1f355c2f24566c 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,6 +3,10 @@ module Banzai
     Renderer.render(text, context)
   end
 
+  def self.render_field(object, field)
+    Renderer.render_field(object, field)
+  end
+
   def self.cache_collection_render(texts_and_contexts)
     Renderer.cache_collection_render(texts_and_contexts)
   end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index d77a5e3ff09099ef5047d2d0517702d75ed595ef..3740d4fb4cd598b047d661126c42d66fbad6114a 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -18,10 +18,6 @@ module Banzai
         @object_sym ||= object_name.to_sym
       end
 
-      def self.object_class_title
-        @object_title ||= object_class.name.titleize
-      end
-
       # Public: Find references in text (like `!123` for merge requests)
       #
       #   AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
@@ -49,10 +45,6 @@ module Banzai
         self.class.object_sym
       end
 
-      def object_class_title
-        self.class.object_class_title
-      end
-
       def references_in(*args, &block)
         self.class.references_in(*args, &block)
       end
@@ -72,7 +64,7 @@ module Banzai
         end
       end
 
-      def project_from_ref_cache(ref)
+      def project_from_ref_cached(ref)
         if RequestStore.active?
           cache = project_refs_cache
 
@@ -110,10 +102,10 @@ module Banzai
             end
 
           elsif element_node?(node)
-            yield_valid_link(node) do |link, text|
+            yield_valid_link(node) do |link, inner_html|
               if ref_pattern && link =~ /\A#{ref_pattern}\z/
                 replace_link_node_with_href(node, link) do
-                  object_link_filter(link, ref_pattern, link_text: text)
+                  object_link_filter(link, ref_pattern, link_content: inner_html)
                 end
 
                 next
@@ -121,9 +113,9 @@ module Banzai
 
               next unless link_pattern
 
-              if link == text && text =~ /\A#{link_pattern}/
+              if link == inner_html && inner_html =~ /\A#{link_pattern}/
                 replace_link_node_with_text(node, link) do
-                  object_link_filter(text, link_pattern)
+                  object_link_filter(inner_html, link_pattern)
                 end
 
                 next
@@ -131,7 +123,7 @@ module Banzai
 
               if link =~ /\A#{link_pattern}\z/
                 replace_link_node_with_href(node, link) do
-                  object_link_filter(link, link_pattern, link_text: text)
+                  object_link_filter(link, link_pattern, link_content: inner_html)
                 end
 
                 next
@@ -148,19 +140,19 @@ module Banzai
       #
       # text - String text to replace references in.
       # pattern - Reference pattern to match against.
-      # link_text - Original content of the link being replaced.
+      # link_content - Original content of the link being replaced.
       #
       # Returns a String with references replaced with links. All links
       # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
-      def object_link_filter(text, pattern, link_text: nil)
+      def object_link_filter(text, pattern, link_content: nil)
         references_in(text, pattern) do |match, id, project_ref, matches|
-          project = project_from_ref_cache(project_ref)
+          project = project_from_ref_cached(project_ref)
 
           if project && object = find_object_cached(project, id)
             title = object_link_title(object)
             klass = reference_class(object_sym)
 
-            data = data_attributes_for(link_text || match, project, object)
+            data = data_attributes_for(link_content || match, project, object)
 
             if matches.names.include?("url") && matches[:url]
               url = matches[:url]
@@ -168,11 +160,11 @@ module Banzai
               url = url_for_object_cached(object, project)
             end
 
-            text = link_text || object_link_text(object, matches)
+            content = link_content || object_link_text(object, matches)
 
             %(<a href="#{url}" #{data}
                  title="#{escape_once(title)}"
-                 class="#{klass}">#{escape_once(text)}</a>)
+                 class="#{klass}">#{content}</a>)
           else
             match
           end
@@ -198,7 +190,7 @@ module Banzai
       end
 
       def object_link_title(object)
-        "#{object_class_title}: #{object.title}"
+        object.title
       end
 
       def object_link_text(object, matches)
@@ -216,8 +208,12 @@ module Banzai
         @references_per_project ||= begin
           refs = Hash.new { |hash, key| hash[key] = Set.new }
 
-          regex = Regexp.union(object_class.reference_pattern,
-                               object_class.link_reference_pattern)
+          regex =
+            if uses_reference_pattern?
+              Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
+            else
+              object_class.link_reference_pattern
+            end
 
           nodes.each do |node|
             node.to_html.scan(regex) do
@@ -251,11 +247,27 @@ module Banzai
         end
       end
 
-      # Returns the projects for the given paths.
-      def find_projects_for_paths(paths)
+      def projects_relation_for_paths(paths)
         Project.where_paths_in(paths).includes(:namespace)
       end
 
+      # Returns projects for the given paths.
+      def find_projects_for_paths(paths)
+        if RequestStore.active?
+          to_query = paths - project_refs_cache.keys
+
+          unless to_query.empty?
+            projects_relation_for_paths(to_query).each do |project|
+              get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
+            end
+          end
+
+          project_refs_cache.slice(*paths).values
+        else
+          projects_relation_for_paths(paths)
+        end
+      end
+
       def current_project_path
         @current_project_path ||= project.path_with_namespace
       end
@@ -287,6 +299,14 @@ module Banzai
           value
         end
       end
+
+      # There might be special cases like filters
+      # that should ignore reference pattern
+      # eg: IssueReferenceFilter when using a external issues tracker
+      # In those cases this method should be overridden on the filter subclass
+      def uses_reference_pattern?
+        true
+      end
     end
   end
 end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 799b83b1069362721c040838f939a58ef94e0cc9..80c844baecd92d42f3acf581ccfbda416620caee 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -71,6 +71,14 @@ module Banzai
         @doc = parse_html(rinku)
       end
 
+      # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
+      def contains_unsafe?(scheme)
+        return false unless scheme
+
+        scheme = scheme.strip.downcase
+        Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
+      end
+
       # Autolinks any text matching LINK_PATTERN that Rinku didn't already
       # replace
       def text_parse
@@ -89,17 +97,27 @@ module Banzai
         doc
       end
 
-      def autolink_filter(text)
-        text.gsub(LINK_PATTERN) do |match|
-          # Remove any trailing HTML entities and store them for appending
-          # outside the link element. The entity must be marked HTML safe in
-          # order to be output literally rather than escaped.
-          match.gsub!(/((?:&[\w#]+;)+)\z/, '')
-          dropped = ($1 || '').html_safe
-
-          options = link_options.merge(href: match)
-          content_tag(:a, match, options) + dropped
+      def autolink_match(match)
+        # start by stripping out dangerous links
+        begin
+          uri = Addressable::URI.parse(match)
+          return match if contains_unsafe?(uri.scheme)
+        rescue Addressable::URI::InvalidURIError
+          return match
         end
+
+        # Remove any trailing HTML entities and store them for appending
+        # outside the link element. The entity must be marked HTML safe in
+        # order to be output literally rather than escaped.
+        match.gsub!(/((?:&[\w#]+;)+)\z/, '')
+        dropped = ($1 || '').html_safe
+
+        options = link_options.merge(href: match)
+        content_tag(:a, match, options) + dropped
+      end
+
+      def autolink_filter(text)
+        text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
       end
 
       def link_options
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index bbb88c979cc3f3c6e4d8d6895bf5e9262666364a..4358bf45549bc2d762a91823b7383000a197ca3a 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -35,7 +35,7 @@ module Banzai
       end
 
       def object_link_title(range)
-        range.reference_title
+        nil
       end
     end
   end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 2ce1816672b93e630244e0048675e4511ff1e56f..a26dd09c25a2f155483fd29666e96c2f0eda9f8d 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -28,10 +28,6 @@ module Banzai
                                         only_path: context[:only_path])
       end
 
-      def object_link_title(commit)
-        commit.link_title
-      end
-
       def object_link_text_extras(object, matches)
         extras = super
 
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 2492b5213ac4230a86bcbabee83246e3ad6b4217..a8c1ca0c60a8a88d8171a961d614d9a9481fca47 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,6 +1,6 @@
 module Banzai
   module Filter
-    # HTML filter that replaces :emoji: with images.
+    # HTML filter that replaces :emoji: and unicode with images.
     #
     # Based on HTML::Pipeline::EmojiFilter
     #
@@ -13,16 +13,17 @@ module Banzai
       def call
         search_text_nodes(doc).each do |node|
           content = node.to_html
-          next unless content.include?(':')
           next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
 
-          html = emoji_image_filter(content)
+          next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
+
+          html = emoji_name_image_filter(content)
+          html = emoji_unicode_image_filter(html)
 
           next if html == content
 
           node.replace(html)
         end
-
         doc
       end
 
@@ -31,18 +32,38 @@ module Banzai
       # text - String text to replace :emoji: in.
       #
       # Returns a String with :emoji: replaced with images.
-      def emoji_image_filter(text)
+      def emoji_name_image_filter(text)
         text.gsub(emoji_pattern) do |match|
           name = $1
-          "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+          emoji_image_tag(name, emoji_url(name))
         end
       end
 
+      # Replace unicode emoji with corresponding images if they exist.
+      #
+      # text - String text to replace unicode emoji in.
+      #
+      # Returns a String with unicode emoji replaced with images.
+      def emoji_unicode_image_filter(text)
+        text.gsub(emoji_unicode_pattern) do |moji|
+          emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+        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('|')}):/
       end
 
+      # Build a regexp that matches all valid unicode emojis names.
+      def self.emoji_unicode_pattern
+        @emoji_unicode_pattern ||= /(#{Gitlab::Emoji.emojis_unicodes.map { |moji| Regexp.escape(moji) }.join('|')})/
+      end
+
       private
 
       def emoji_url(name)
@@ -60,6 +81,18 @@ module Banzai
         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
@@ -71,6 +104,14 @@ module Banzai
       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/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index eaa702952ccbeca8b4fc7c76e698ce0a7b9776cf..dce4de3ceaf800acb5dc6d1237f8646796046d70 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -8,7 +8,7 @@ module Banzai
 
       # Public: Find `JIRA-123` issue references in text
       #
-      #   ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+      #   ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
       #     "<a href=...>##{issue}</a>"
       #   end
       #
@@ -17,8 +17,8 @@ module Banzai
       # Yields the String match and the String issue reference.
       #
       # Returns a String replaced with the return of the block.
-      def self.references_in(text)
-        text.gsub(ExternalIssue.reference_pattern) do |match|
+      def self.references_in(text, pattern)
+        text.gsub(pattern) do |match|
           yield match, $~[:issue]
         end
       end
@@ -27,7 +27,7 @@ module Banzai
         # Early return if the project isn't using an external tracker
         return doc if project.nil? || default_issues_tracker?
 
-        ref_pattern = ExternalIssue.reference_pattern
+        ref_pattern = issue_reference_pattern
         ref_start_pattern = /\A#{ref_pattern}\z/
 
         each_node do |node|
@@ -37,10 +37,10 @@ module Banzai
             end
 
           elsif element_node?(node)
-            yield_valid_link(node) do |link, text|
+            yield_valid_link(node) do |link, inner_html|
               if link =~ ref_start_pattern
                 replace_link_node_with_href(node, link) do
-                  issue_link_filter(link, link_text: text)
+                  issue_link_filter(link, link_content: inner_html)
                 end
               end
             end
@@ -54,13 +54,14 @@ module Banzai
       # issue's details page.
       #
       # text - String text to replace references in.
+      # link_content - Original content of the link being replaced.
       #
       # Returns a String with `JIRA-123` references replaced with links. All
       # links have `gfm` and `gfm-issue` class names attached for styling.
-      def issue_link_filter(text, link_text: nil)
+      def issue_link_filter(text, link_content: nil)
         project = context[:project]
 
-        self.class.references_in(text) do |match, id|
+        self.class.references_in(text, issue_reference_pattern) do |match, id|
           ExternalIssue.new(id, project)
 
           url = url_for_issue(id, project, only_path: context[:only_path])
@@ -69,11 +70,11 @@ module Banzai
           klass = reference_class(:issue)
           data  = data_attribute(project: project.id, external_issue: id)
 
-          text = link_text || match
+          content = link_content || match
 
           %(<a href="#{url}" #{data}
                title="#{escape_once(title)}"
-               class="#{klass}">#{escape_once(text)}</a>)
+               class="#{klass}">#{content}</a>)
         end
       end
 
@@ -82,18 +83,21 @@ module Banzai
       end
 
       def default_issues_tracker?
-        if RequestStore.active?
-          default_issues_tracker_cache[project.id] ||=
-            project.default_issues_tracker?
-        else
-          project.default_issues_tracker?
-        end
+        external_issues_cached(:default_issues_tracker?)
+      end
+
+      def issue_reference_pattern
+        external_issues_cached(:issue_reference_pattern)
       end
 
       private
 
-      def default_issues_tracker_cache
-        RequestStore[:banzai_default_issues_tracker_cache] ||= {}
+      def external_issues_cached(attribute)
+        return project.public_send(attribute) unless RequestStore.active?
+
+        cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
+        cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil?
+        cached_attributes[project.id][attribute]
       end
     end
   end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 0a29c547a4de70ec177eed8fc1112f0882654e4a..2f19b59e7252de0701adcfec839f412eb9415d27 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,10 +3,17 @@ module Banzai
     # HTML Filter to modify the attributes of external links
     class ExternalLinkFilter < HTML::Pipeline::Filter
       def call
-        # Skip non-HTTP(S) links and internal links
-        doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
-          node.set_attribute('rel', 'nofollow noreferrer')
-          node.set_attribute('target', '_blank')
+        links.each do |node|
+          href = href_to_lowercase_scheme(node["href"].to_s)
+
+          unless node["href"].to_s == href
+            node.set_attribute('href', href)
+          end
+
+          if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+            node.set_attribute('rel', 'nofollow noreferrer')
+            node.set_attribute('target', '_blank')
+          end
         end
 
         doc
@@ -14,6 +21,25 @@ module Banzai
 
       private
 
+      def links
+        query = 'descendant-or-self::a[@href and not(@href = "")]'
+        doc.xpath(query)
+      end
+
+      def href_to_lowercase_scheme(href)
+        scheme_match = href.match(/\A(\w+):\/\//)
+
+        if scheme_match
+          scheme_match.to_s.downcase + scheme_match.post_match
+        else
+          href
+        end
+      end
+
+      def external_url?(url)
+        !url.start_with?(internal_url)
+      end
+
       def internal_url
         @internal_url ||= Gitlab.config.gitlab.url
       end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f3bd587c28bbf92a333d408a4b2bdf5a6ddc538e
--- /dev/null
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -0,0 +1,12 @@
+require 'erb'
+
+module Banzai
+  module Filter
+    # Text filter that escapes these HTML entities: & " < >
+    class HtmlEntityFilter < HTML::Pipeline::TextFilter
+      def call
+        ERB::Util.html_escape_once(text)
+      end
+    end
+  end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 4042e9a4c250f0bc29c307ba73451cbff6d5564f..4d1bc687696f993eda7e6b77337af1cc2d447b2b 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -4,6 +4,10 @@ module Banzai
     # issues that do not exist are ignored.
     #
     # This filter supports cross-project references.
+    #
+    # When external issues tracker like Jira is activated we should not
+    # use issue reference pattern, but we should still be able
+    # to reference issues from other GitLab projects.
     class IssueReferenceFilter < AbstractReferenceFilter
       self.reference_type = :issue
 
@@ -11,6 +15,10 @@ module Banzai
         Issue
       end
 
+      def uses_reference_pattern?
+        context[:project].default_issues_tracker?
+      end
+
       def find_object(project, iid)
         issues_per_project[project][iid]
       end
@@ -66,7 +74,7 @@ module Banzai
         end
       end
 
-      def find_projects_for_paths(paths)
+      def projects_relation_for_paths(paths)
         super(paths).includes(:gitlab_issue_tracker_service)
       end
     end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index e258dc8e2bf017d02bd637d0b6e2a4803bcee2cb..9f9a96cdc6564a62165ca10a451793e39ce7dd68 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -9,7 +9,7 @@ module Banzai
       end
 
       def find_object(project, id)
-        project.labels.find(id)
+        find_labels(project).find(id)
       end
 
       def self.references_in(text, pattern = Label.reference_pattern)
@@ -35,7 +35,11 @@ module Banzai
         return unless project
 
         label_params = label_params(label_id, label_name)
-        project.labels.find_by(label_params)
+        find_labels(project).find_by(label_params)
+      end
+
+      def find_labels(project)
+        LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true)
       end
 
       # Parameters to pass to `Label.find_by` based on the given arguments
@@ -60,16 +64,58 @@ module Banzai
       end
 
       def object_link_text(object, matches)
-        if context[:project] == object.project
-          LabelsHelper.render_colored_label(object)
+        if same_group?(object) && namespace_match?(matches)
+          render_same_project_label(object)
+        elsif same_project?(object)
+          render_same_project_label(object)
         else
-          LabelsHelper.render_colored_cross_project_label(object)
+          render_cross_project_label(object, matches)
         end
       end
 
+      def same_group?(object)
+        object.is_a?(GroupLabel) && object.group == project.group
+      end
+
+      def namespace_match?(matches)
+        matches[:project].blank? || matches[:project] == project.path_with_namespace
+      end
+
+      def same_project?(object)
+        object.is_a?(ProjectLabel) && object.project == project
+      end
+
+      def user
+        context[:current_user] || context[:author]
+      end
+
+      def project
+        context[:project]
+      end
+
+      def render_same_project_label(object)
+        LabelsHelper.render_colored_label(object)
+      end
+
+      def render_cross_project_label(object, matches)
+        source_project =
+          if matches[:project]
+            Project.find_with_namespace(matches[:project])
+          else
+            object.project
+          end
+
+        LabelsHelper.render_colored_cross_project_label(object, source_project)
+      end
+
       def unescape_html_entities(text)
         CGI.unescapeHTML(text.to_s)
       end
+
+      def object_link_title(object)
+        # use title of wrapped element instead
+        nil
+      end
     end
   end
 end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index ca686c87d97bd9ef628c40b7d525c8cebcefe608..58fff496d003d765149262e89f6f152a2816026c 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -59,6 +59,10 @@ module Banzai
             html_safe
         end
       end
+
+      def object_link_title(object)
+        nil
+      end
     end
   end
 end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index bf058241cda706d8896b8ad76ab2103b3cc26375..84bfeac80417a21ef7fcbcec13f9b1a5bfaafdf3 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -52,7 +52,7 @@ module Banzai
       end
 
       def reference_class(type)
-        "gfm gfm-#{type}"
+        "gfm gfm-#{type} has-tooltip"
       end
 
       # Ensure that a :project key exists in context
@@ -85,14 +85,14 @@ module Banzai
         @nodes ||= each_node.to_a
       end
 
-      # Yields the link's URL and text whenever the node is a valid <a> tag.
+      # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
       def yield_valid_link(node)
         link = CGI.unescape(node.attr('href').to_s)
-        text = node.text
+        inner_html = node.inner_html
 
         return unless link.force_encoding('UTF-8').valid_encoding?
 
-        yield link, text
+        yield link, inner_html
       end
 
       def replace_text_when_pattern_matches(node, pattern)
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 4fa8d05481f991cb7ba62c58f24d05abf2be635f..f09d78be0cee191ca461f0f9b625e79a93da1349 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -52,8 +52,8 @@ module Banzai
           relative_url_root,
           context[:project].path_with_namespace,
           uri_type(file_path),
-          ref,
-          file_path
+          Addressable::URI.escape(ref),
+          Addressable::URI.escape(file_path)
         ].compact.join('/').squeeze('/').chomp('/')
 
         uri
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 6e13282d5f4ebb18376f9aec2be117544948f2a0..af1e575fc89d6a9270c9e07a58a9eba7fda372b4 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
       UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
 
       def whitelist
-        whitelist = super.dup
+        whitelist = super
 
         customize_whitelist(whitelist)
 
@@ -25,7 +25,7 @@ module Banzai
         return if customized?(whitelist[:transformers])
 
         # Allow code highlighting
-        whitelist[:attributes]['pre'] = %w(class)
+        whitelist[:attributes]['pre'] = %w(class v-pre)
         whitelist[:attributes]['span'] = %w(class)
 
         # Allow table alignment
@@ -42,58 +42,58 @@ module Banzai
         # Allow any protocol in `a` elements...
         whitelist[:protocols].delete('a')
 
-        whitelist[:transformers] = whitelist[:transformers].dup
-
         # ...but then remove links with unsafe protocols
-        whitelist[:transformers].push(remove_unsafe_links)
+        whitelist[:transformers].push(self.class.remove_unsafe_links)
 
         # Remove `rel` attribute from `a` elements
-        whitelist[:transformers].push(remove_rel)
+        whitelist[:transformers].push(self.class.remove_rel)
 
         # Remove `class` attribute from non-highlight spans
-        whitelist[:transformers].push(clean_spans)
+        whitelist[:transformers].push(self.class.clean_spans)
 
         whitelist
       end
 
-      def remove_unsafe_links
-        lambda do |env|
-          node = env[:node]
+      class << self
+        def remove_unsafe_links
+          lambda do |env|
+            node = env[:node]
 
-          return unless node.name == 'a'
-          return unless node.has_attribute?('href')
+            return unless node.name == 'a'
+            return unless node.has_attribute?('href')
 
-          begin
-            uri = Addressable::URI.parse(node['href'])
-            uri.scheme = uri.scheme.strip.downcase if uri.scheme
+            begin
+              uri = Addressable::URI.parse(node['href'])
+              uri.scheme = uri.scheme.strip.downcase if uri.scheme
 
-            node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
-          rescue Addressable::URI::InvalidURIError
-            node.remove_attribute('href')
+              node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+            rescue Addressable::URI::InvalidURIError
+              node.remove_attribute('href')
+            end
           end
         end
-      end
 
-      def remove_rel
-        lambda do |env|
-          if env[:node_name] == 'a'
-            env[:node].remove_attribute('rel')
+        def remove_rel
+          lambda do |env|
+            if env[:node_name] == 'a'
+              env[:node].remove_attribute('rel')
+            end
           end
         end
-      end
 
-      def clean_spans
-        lambda do |env|
-          node = env[:node]
+        def clean_spans
+          lambda do |env|
+            node = env[:node]
 
-          return unless node.name == 'span'
-          return unless node.has_attribute?('class')
+            return unless node.name == 'span'
+            return unless node.has_attribute?('class')
 
-          unless has_ancestor?(node, 'pre')
-            node.remove_attribute('class')
-          end
+            unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? }
+              node.remove_attribute('class')
+            end
 
-          { node_whitelist: [node] }
+            { node_whitelist: [node] }
+          end
         end
       end
     end
diff --git a/lib/banzai/filter/set_direction_filter.rb b/lib/banzai/filter/set_direction_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c2976aeb7c6dff663fa95e389722b2ed2a7cdc4f
--- /dev/null
+++ b/lib/banzai/filter/set_direction_filter.rb
@@ -0,0 +1,15 @@
+module Banzai
+  module Filter
+    # HTML filter that sets dir="auto" for RTL languages support
+    class SetDirectionFilter < HTML::Pipeline::Filter
+      def call
+        # select these elements just on top level of the document
+        doc.xpath('p|h1|h2|h3|h4|h5|h6|ol|ul[not(@class="section-nav")]|blockquote|table').each do |el|
+          el['dir'] = 'auto'
+        end
+
+        doc
+      end
+    end
+  end
+end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index fcdb496aed245c4321c4ffded8dedfdf257a3129..026b81ac17549c1e4ca53fd3785555372b684036 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -30,7 +30,7 @@ module Banzai
           # users can still access an issue/comment/etc.
         end
 
-        highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>)
+        highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
 
         # Extracted to a method to measure it
         replace_parent_pre_element(node, highlighted)
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 66608c9859c1087d2008baf4eb2f6660ad2b98b0..9fa5f589f3ec258aed87bae0863a45b66a45908a 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -2,27 +2,7 @@ require 'task_list/filter'
 
 module Banzai
   module Filter
-    # Work around a bug in the default TaskList::Filter that adds a `task-list`
-    # class to every list element, regardless of whether or not it contains a
-    # task list.
-    #
-    # This is a (hopefully) temporary fix, pending a new release of the
-    # task_list gem.
-    #
-    # See https://github.com/github/task_list/pull/60
     class TaskListFilter < TaskList::Filter
-      def add_css_class_with_fix(node, *new_class_names)
-        if new_class_names.include?('task-list')
-          # Don't add class to all lists
-          return
-        elsif new_class_names.include?('task-list-item')
-          add_css_class_without_fix(node.parent, 'task-list')
-        end
-
-        add_css_class_without_fix(node, *new_class_names)
-      end
-
-      alias_method_chain :add_css_class, :fix
     end
   end
 end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index e1ca7f4d24b8f8e9da7bb579d07fa9dd144b14ab..f842b1fb779d7b79a9ade9f80814b7eb54ed3847 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -35,10 +35,10 @@ module Banzai
               user_link_filter(content)
             end
           elsif element_node?(node)
-            yield_valid_link(node) do |link, text|
+            yield_valid_link(node) do |link, inner_html|
               if link =~ ref_pattern_start
                 replace_link_node_with_href(node, link) do
-                  user_link_filter(link, link_text: text)
+                  user_link_filter(link, link_content: inner_html)
                 end
               end
             end
@@ -52,15 +52,16 @@ module Banzai
       # user's profile page.
       #
       # text - String text to replace references in.
+      # link_content - Original content of the link being replaced.
       #
       # Returns a String with `@user` references replaced with links. All links
       # have `gfm` and `gfm-project_member` class names attached for styling.
-      def user_link_filter(text, link_text: nil)
+      def user_link_filter(text, link_content: nil)
         self.class.references_in(text) do |match, username|
           if username == 'all'
-            link_to_all(link_text: link_text)
+            link_to_all(link_content: link_content)
           elsif namespace = namespaces[username]
-            link_to_namespace(namespace, link_text: link_text) || match
+            link_to_namespace(namespace, link_content: link_content) || match
           else
             match
           end
@@ -102,45 +103,49 @@ module Banzai
         reference_class(:project_member)
       end
 
-      def link_to_all(link_text: nil)
+      def link_to_all(link_content: nil)
         project = context[:project]
         author = context[:author]
 
-        url = urls.namespace_project_url(project.namespace, project,
-                                         only_path: context[:only_path])
+        if author && !project.team.member?(author)
+          link_content
+        else
+          url = urls.namespace_project_url(project.namespace, project,
+                                           only_path: context[:only_path])
 
-        data = data_attribute(project: project.id, author: author.try(:id))
-        text = link_text || User.reference_prefix + 'all'
+          data = data_attribute(project: project.id, author: author.try(:id))
+          content = link_content || User.reference_prefix + 'all'
 
-        link_tag(url, data, text, 'All Project and Group Members')
+          link_tag(url, data, content, 'All Project and Group Members')
+        end
       end
 
-      def link_to_namespace(namespace, link_text: nil)
+      def link_to_namespace(namespace, link_content: nil)
         if namespace.is_a?(Group)
-          link_to_group(namespace.path, namespace, link_text: link_text)
+          link_to_group(namespace.path, namespace, link_content: link_content)
         else
-          link_to_user(namespace.path, namespace, link_text: link_text)
+          link_to_user(namespace.path, namespace, link_content: link_content)
         end
       end
 
-      def link_to_group(group, namespace, link_text: nil)
+      def link_to_group(group, namespace, link_content: nil)
         url = urls.group_url(group, only_path: context[:only_path])
         data = data_attribute(group: namespace.id)
-        text = link_text || Group.reference_prefix + group
+        content = link_content || Group.reference_prefix + group
 
-        link_tag(url, data, text, namespace.name)
+        link_tag(url, data, content, namespace.name)
       end
 
-      def link_to_user(user, namespace, link_text: nil)
+      def link_to_user(user, namespace, link_content: nil)
         url = urls.user_url(user, only_path: context[:only_path])
         data = data_attribute(user: namespace.owner_id)
-        text = link_text || User.reference_prefix + user
+        content = link_content || User.reference_prefix + user
 
-        link_tag(url, data, text, namespace.owner_name)
+        link_tag(url, data, content, namespace.owner_name)
       end
 
-      def link_tag(url, data, text, title)
-        %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>)
+      def link_tag(url, data, link_content, title)
+        %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
       end
     end
   end
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index 2e2c8da311e433fccf970748061ce99366c1d028..e7a1ec8457de2e5c8f4337599f3a6ace1b26b48f 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -31,6 +31,7 @@ module Banzai
         def apply_relative_link_rules!
           if @uri.relative? && @uri.path.present?
             link = ::File.join(@wiki_base_path, @uri.path)
+            link = "#{link}##{@uri.fragment}" if @uri.fragment
             @uri = Addressable::URI.parse(link)
           end
         end
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
index bab6a9934d171ca4213f42e3db37ed79d7b8a0c7..2b7c10f1a0ecf15f04339328ed72ec6ef6312ef1 100644
--- a/lib/banzai/note_renderer.rb
+++ b/lib/banzai/note_renderer.rb
@@ -3,7 +3,7 @@ module Banzai
     # Renders a collection of Note instances.
     #
     # notes - The notes to render.
-    # project - The project to use for rendering/redacting.
+    # project - The project to use for redacting.
     # user - The user viewing the notes.
     # path - The request path.
     # wiki - The project's wiki.
@@ -13,8 +13,7 @@ module Banzai
                                     user,
                                     requested_path: path,
                                     project_wiki: wiki,
-                                    ref: git_ref,
-                                    pipeline: :note)
+                                    ref: git_ref)
 
       renderer.render(notes, :note)
     end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 9aef807c1528d2b66658fc5b68dd82acbf741504..9f8eb0931b8709d00bd4a26aa19e510c7e4fbc31 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -1,28 +1,32 @@
 module Banzai
-  # Class for rendering multiple objects (e.g. Note instances) in a single pass.
+  # Class for rendering multiple objects (e.g. Note instances) in a single pass,
+  # using +render_field+ to benefit from caching in the database. Rendering and
+  # redaction are both performed.
   #
-  # Rendered Markdown is stored in an attribute in every object based on the
-  # name of the attribute containing the Markdown. For example, when the
-  # attribute `note` is rendered the HTML is stored in `note_html`.
+  # The unredacted HTML is generated according to the usual +render_field+
+  # policy, so specify the pipeline and any other context options on the model.
+  #
+  # The *redacted* (i.e., suitable for use) HTML is placed in an attribute
+  # named "redacted_<foo>", where <foo> is the name of the cache field for the
+  # chosen attribute.
+  #
+  # As an example, rendering the attribute `note` would place the unredacted
+  # HTML into `note_html` and the redacted HTML into `redacted_note_html`.
   class ObjectRenderer
     attr_reader :project, :user
 
-    # Make sure to set the appropriate pipeline in the `raw_context` attribute
-    # (e.g. `:note` for Note instances).
-    #
-    # project - A Project to use for rendering and redacting Markdown.
+    # project - A Project to use for redacting Markdown.
     # user - The user viewing the Markdown/HTML documents, if any.
-    # context - A Hash containing extra attributes to use in the rendering
-    #           pipeline.
-    def initialize(project, user = nil, raw_context = {})
+    # context - A Hash containing extra attributes to use during redaction
+    def initialize(project, user = nil, redaction_context = {})
       @project = project
       @user = user
-      @raw_context = raw_context
+      @redaction_context = redaction_context
     end
 
     # Renders and redacts an Array of objects.
     #
-    # objects - The objects to render
+    # objects - The objects to render.
     # attribute - The attribute containing the raw Markdown to render.
     #
     # Returns the same input objects.
@@ -32,7 +36,7 @@ module Banzai
 
       objects.each_with_index do |object, index|
         redacted_data = redacted[index]
-        object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe)
+        object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe)
         object.user_visible_reference_count = redacted_data[:visible_reference_count]
       end
     end
@@ -53,12 +57,8 @@ module Banzai
 
     # Returns a Banzai context for the given object and attribute.
     def context_for(object, attribute)
-      context = base_context.merge(cache_key: [object, attribute])
-
-      if object.respond_to?(:author)
-        context[:author] = object.author
-      end
-
+      context = base_context.dup
+      context = context.merge(object.banzai_render_context(attribute))
       context
     end
 
@@ -66,21 +66,16 @@ module Banzai
     #
     # Returns an Array of `Nokogiri::HTML::Document`.
     def render_attributes(objects, attribute)
-      strings_and_contexts = objects.map do |object|
+      objects.map do |object|
+        string = Banzai.render_field(object, attribute)
         context = context_for(object, attribute)
 
-        string = object.__send__(attribute)
-
-        { text: string, context: context }
-      end
-
-      Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index|
-        Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context])
+        Banzai::Pipeline[:relative_link].to_document(string, context)
       end
     end
 
     def base_context
-      @base_context ||= @raw_context.merge(current_user: user, project: project)
+      @base_context ||= @redaction_context.merge(current_user: user, project: project)
     end
   end
 end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8d94b199c6680194be3b8ff2b53e37f63de0d865..5da2d0b008c860ad9c27c49506aabcc05ce2c6b4 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -25,7 +25,9 @@ module Banzai
           Filter::MilestoneReferenceFilter,
 
           Filter::TaskListFilter,
-          Filter::InlineDiffFilter
+          Filter::InlineDiffFilter,
+
+          Filter::SetDirectionFilter
         ]
       end
 
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index ba2555df98d596c9794ca7f15c59694926b8a08c..1929099931bf97e4bc1948f0c84a6b43ba0099d5 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -3,6 +3,7 @@ module Banzai
     class SingleLinePipeline < GfmPipeline
       def self.filters
         @filters ||= FilterArray[
+          Filter::HtmlEntityFilter,
           Filter::SanitizationFilter,
 
           Filter::EmojiFilter,
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 0df3a72d1c46d3395ea7d9744cf5c93a5c4d004a..de3ebe72720e698849ef1a8d99bdf49e374b6cc5 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -41,10 +41,10 @@ module Banzai
           next if visible.include?(node)
 
           doc_data[:visible_reference_count] -= 1
-          # The reference should be replaced by the original text,
-          # which is not always the same as the rendered text.
-          text = node.attr('data-original') || node.text
-          node.replace(text)
+          # The reference should be replaced by the original link's content,
+          # which is not always the same as the rendered one.
+          content = node.attr('data-original') || node.inner_html
+          node.replace(content)
         end
       end
 
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 6cf218aaa0d304022ba1ce91c9f9716bca50add1..d8a855ec1fe849d3751483a0391799703ea0255b 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -63,12 +63,7 @@ module Banzai
         nodes.select do |node|
           if node.has_attribute?(project_attr)
             node_id = node.attr(project_attr).to_i
-
-            if project && project.id == node_id
-              true
-            else
-              can?(user, :read_project, projects[node_id])
-            end
+            can_read_reference?(user, projects[node_id])
           else
             true
           end
@@ -79,7 +74,11 @@ module Banzai
       def referenced_by(nodes)
         ids = unique_attribute_values(nodes, self.class.data_attribute)
 
-        references_relation.where(id: ids)
+        if ids.empty?
+          references_relation.none
+        else
+          references_relation.where(id: ids)
+        end
       end
 
       # Returns the ActiveRecord::Relation to use for querying references in the
@@ -211,7 +210,7 @@ module Banzai
       end
 
       def can?(user, permission, subject)
-        Ability.abilities.allowed?(user, permission, subject)
+        Ability.allowed?(user, permission, subject)
       end
 
       def find_projects_for_hash_keys(hash)
@@ -222,6 +221,15 @@ module Banzai
 
       attr_reader :current_user, :project
 
+      # When a feature is disabled or visible only for
+      # team members we should not allow team members
+      # see reference comments.
+      # Override this method on subclasses
+      # to check if user can read resource
+      def can_read_reference?(user, ref_project)
+        raise NotImplementedError
+      end
+
       def lazy(&block)
         Gitlab::Lazy.new(&block)
       end
diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb
index 0fee9d267dee639dea5682204e292edb950317d7..8c54a041cb8164dfc9ac6e7bf8d91cee91296de2 100644
--- a/lib/banzai/reference_parser/commit_parser.rb
+++ b/lib/banzai/reference_parser/commit_parser.rb
@@ -29,6 +29,12 @@ module Banzai
 
         commits
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :download_code, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
index 69d01f8db1577c67ecaf7cfe4a5880f57688bdbb..0878b6afba3b69dc5cf375098d27e59d9028132a 100644
--- a/lib/banzai/reference_parser/commit_range_parser.rb
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -33,6 +33,12 @@ module Banzai
 
         range.valid_commits? ? range : nil
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :download_code, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
index a1264db21115f0ff19d3997e8cd7ad41337881c1..6e7b7669578471bc32c3ec1e34b0c71b4a095b39 100644
--- a/lib/banzai/reference_parser/external_issue_parser.rb
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -20,6 +20,12 @@ module Banzai
       def issue_ids_per_project(nodes)
         gather_attributes_per_project(nodes, self.class.data_attribute)
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_issue, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb
index e5d1eb11d7f5d38c9256c124568cf07d5a5391ab..aa76c64ac5f1939de39efe3d390f8b1a37183aab 100644
--- a/lib/banzai/reference_parser/label_parser.rb
+++ b/lib/banzai/reference_parser/label_parser.rb
@@ -6,6 +6,12 @@ module Banzai
       def references_relation
         Label
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_label, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index c9a9ca79c09c52c41b4654af68b06589162a9843..40451947e6c0663a258cde19aca6b6518e982be2 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -6,6 +6,12 @@ module Banzai
       def references_relation
         MergeRequest.includes(:author, :assignee, :target_project)
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_merge_request, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb
index a000ac61e5c3f809974e9e2e4a9b5d66f45be0b0..d3968d6b229316711b68fc70b9248834aed90fe1 100644
--- a/lib/banzai/reference_parser/milestone_parser.rb
+++ b/lib/banzai/reference_parser/milestone_parser.rb
@@ -6,6 +6,12 @@ module Banzai
       def references_relation
         Milestone
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_milestone, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
index fa71b3c952aa3fadf1b357c3a286464a44d1c3d0..63b592137bb66c2a79c0fd4cbeb043cd559c92ed 100644
--- a/lib/banzai/reference_parser/snippet_parser.rb
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -6,6 +6,12 @@ module Banzai
       def references_relation
         Snippet
       end
+
+      private
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_project_snippet, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 863f5725d3bcfc3a8f4df956b40fbe381339ae4e..7adaffa19c19ad7546b4b05cd21fd914768be26a 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -30,22 +30,36 @@ module Banzai
 
         nodes.each do |node|
           if node.has_attribute?(group_attr)
-            node_group = groups[node.attr(group_attr).to_i]
-
-            if node_group &&
-              can?(user, :read_group, node_group)
-              visible << node
-            end
-          # Remaining nodes will be processed by the parent class'
-          # implementation of this method.
+            next unless can_read_group_reference?(node, user, groups)
+            visible << node
+          elsif can_read_project_reference?(node)
+            visible << node
           else
             remaining << node
           end
         end
 
+        # If project does not belong to a group
+        # and does not have the same project id as the current project
+        # base class will check if user can read the project that contains
+        # the user reference.
         visible + super(current_user, remaining)
       end
 
+      # Check if project belongs to a group which
+      # user can read.
+      def can_read_group_reference?(node, user, groups)
+        node_group = groups[node.attr('data-group').to_i]
+
+        node_group && can?(user, :read_group, node_group)
+      end
+
+      def can_read_project_reference?(node)
+        node_id = node.attr('data-project').to_i
+
+        project && project.id == node_id
+      end
+
       def nodes_user_can_reference(current_user, nodes)
         project_attr = 'data-project'
         author_attr = 'data-author'
@@ -88,6 +102,10 @@ module Banzai
         collection_objects_for_ids(Project, ids).
           flat_map { |p| p.team.members.to_a }
       end
+
+      def can_read_reference?(user, ref_project)
+        can?(user, :read_project, ref_project)
+      end
     end
   end
 end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index a4ae27eefd81012547a9bae0952ab74c71cf6721..f31fb6c3f71371783be9460dd71dca08ac887497 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,6 +1,6 @@
 module Banzai
   module Renderer
-    extend self
+    module_function
 
     # Convert a Markdown String into an HTML-safe String of HTML
     #
@@ -31,6 +31,34 @@ module Banzai
       end
     end
 
+    # Convert a Markdown-containing field on an object into an HTML-safe String
+    # of HTML. This method is analogous to calling render(object.field), but it
+    # can cache the rendered HTML in the object, rather than Redis.
+    #
+    # The context to use is learned from the passed-in object by calling
+    # #banzai_render_context(field), and cannot be changed. Use #render, passing
+    # it the field text, if a custom rendering is needed. The generated context
+    # is returned along with the HTML.
+    def render_field(object, field)
+      html_field = object.markdown_cache_field_for(field)
+
+      html = object.__send__(html_field)
+      return html if html.present?
+
+      html = cacheless_render_field(object, field)
+      update_object(object, html_field, html) unless object.new_record? || object.destroyed?
+
+      html
+    end
+
+    # Same as +render_field+, but without consulting or updating the cache field
+    def cacheless_render_field(object, field)
+      text = object.__send__(field)
+      context = object.banzai_render_context(field)
+
+      cacheless_render(text, context)
+    end
+
     # Perform multiple render from an Array of Markdown String into an
     # Array of HTML-safe String of HTML.
     #
@@ -113,8 +141,6 @@ module Banzai
       end.html_safe
     end
 
-    private
-
     def cacheless_render(text, context = {})
       Gitlab::Metrics.measure(:banzai_cacheless_render) do
         result = render_result(text, context)
@@ -140,5 +166,9 @@ module Banzai
       return unless cache_key
       Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
     end
+
+    def update_object(object, html_field, html)
+      object.update_column(html_field, html)
+    end
   end
 end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 17bb99a2ae50248da39cb42fe1e545daa8b7f57e..a6b9beecded18128ccd4ee5d3b7217e05918edff 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -9,22 +9,14 @@ module Ci
       end
 
       rescue_from :all do |exception|
-        # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
-        # why is this not wrapped in something reusable?
-        trace = exception.backtrace
-
-        message = "\n#{exception.class} (#{exception.message}):\n"
-        message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
-        message << "  " << trace.join("\n  ")
-
-        API.logger.add Logger::FATAL, message
-        rack_response({ 'message' => '500 Internal Server Error' }, 500)
+        handle_api_exception(exception)
       end
 
       content_type :txt,  'text/plain'
       content_type :json, 'application/json'
       format :json
 
+      helpers ::SentryHelper
       helpers ::Ci::API::Helpers
       helpers ::API::Helpers
       helpers Gitlab::CurrentSettings
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 260ac81f5fab8ea822a250a664d8426f2ab8b7a0..ed87a2603e842c504dc784dc07cd987c62db8a61 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -12,17 +12,21 @@ module Ci
         #   POST /builds/register
         post "register" do
           authenticate_runner!
-          update_runner_last_contact
-          update_runner_info
           required_attributes! [:token]
           not_found! unless current_runner.active?
+          update_runner_info
 
           build = Ci::RegisterBuildService.new.execute(current_runner)
 
           if build
+            Gitlab::Metrics.add_event(:build_found,
+                                      project: build.project.path_with_namespace)
+
             present build, with: Entities::BuildDetails
           else
-            not_found!
+            Gitlab::Metrics.add_event(:build_not_found)
+
+            build_not_found!
           end
         end
 
@@ -36,12 +40,16 @@ module Ci
         #   PUT /builds/:id
         put ":id" do
           authenticate_runner!
-          update_runner_last_contact
           build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
           forbidden!('Build has been erased!') if build.erased?
 
+          update_runner_info
+
           build.update_attributes(trace: params[:trace]) if params[:trace]
 
+          Gitlab::Metrics.add_event(:update_build,
+                                    project: build.project.path_with_namespace)
+
           case params[:state].to_s
           when 'success'
             build.success
@@ -93,6 +101,7 @@ module Ci
         #   POST /builds/:id/artifacts/authorize
         post ":id/artifacts/authorize" do
           require_gitlab_workhorse!
+          Gitlab::Workhorse.verify_api_request!(headers)
           not_allowed! unless Gitlab.config.artifacts.enabled
           build = Ci::Build.find_by_id(params[:id])
           not_found! unless build
@@ -105,7 +114,8 @@ module Ci
           end
 
           status 200
-          { TempPath: ArtifactUploader.artifacts_upload_path }
+          content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+          Gitlab::Workhorse.artifact_upload_ok
         end
 
         # Upload artifacts to build - Runners only
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index 3f5bdaba3f5d0f757e23411ce83c72282fe54d52..66c05773b68451bbd438855369d385702f89afa6 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -15,6 +15,15 @@ module Ci
         expose :filename, :size
       end
 
+      class BuildOptions < Grape::Entity
+        expose :image
+        expose :services
+        expose :artifacts
+        expose :cache
+        expose :dependencies
+        expose :after_script
+      end
+
       class Build < Grape::Entity
         expose :id, :ref, :tag, :sha, :status
         expose :name, :token, :stage
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 199d62d9b8a1c2f08f2182fc1d1444b117f11ae5..e608f5f6cade3394fa7c085d982f541f35626cd6 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -3,7 +3,7 @@ module Ci
     module Helpers
       BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
       BUILD_TOKEN_PARAM = :token
-      UPDATE_RUNNER_EVERY = 60
+      UPDATE_RUNNER_EVERY = 10 * 60
 
       def authenticate_runners!
         forbidden! unless runner_registration_token_valid?
@@ -14,19 +14,45 @@ module Ci
       end
 
       def authenticate_build_token!(build)
-        token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
-        forbidden! unless token && build.valid_token?(token)
+        forbidden! unless build_token_valid?(build)
       end
 
       def runner_registration_token_valid?
-        params[:token] == current_application_settings.runners_registration_token
+        ActiveSupport::SecurityUtils.variable_size_secure_compare(
+          params[:token],
+          current_application_settings.runners_registration_token)
+      end
+
+      def build_token_valid?(build)
+        token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
+
+        # We require to also check `runners_token` to maintain compatibility with old version of runners
+        token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
       end
 
-      def update_runner_last_contact
-        # Use a random threshold to prevent beating DB updates
+      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)
-        if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
-          current_runner.update_attributes(contacted_at: Time.now)
+
+        current_runner.contacted_at.nil? ||
+          (Time.now - current_runner.contacted_at) >= contacted_at_max_age
+      end
+
+      def build_not_found!
+        if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+          no_content!
+        else
+          not_found!
         end
       end
 
@@ -39,11 +65,6 @@ module Ci
         attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
       end
 
-      def update_runner_info
-        current_runner.assign_attributes(get_runner_version_from_params)
-        current_runner.save if current_runner.changed?
-      end
-
       def max_artifacts_size
         current_application_settings.max_artifacts_size.megabytes.to_i
       end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 47efd5bd9f264eb3685d0f07e252bff49dedb8f5..3e33c9399e23506d5eedb20ab8b2ccaa7aee62dc 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,7 +4,7 @@ module Ci
 
     include Gitlab::Ci::Config::Node::LegacyValidationHelpers
 
-    attr_reader :path, :cache, :stages
+    attr_reader :path, :cache, :stages, :jobs
 
     def initialize(config, path = nil)
       @ci_config = Gitlab::Ci::Config.new(config)
@@ -55,29 +55,36 @@ module Ci
       {
         stage_idx: @stages.index(job[:stage]),
         stage: job[:stage],
-        ##
-        # Refactoring note:
-        #  - before script behaves differently than after script
-        #  - after script returns an array of commands
-        #  - before script should be a concatenated command
-        commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
+        commands: job[:commands],
         tag_list: job[:tags] || [],
         name: job[:name].to_s,
         allow_failure: job[:allow_failure] || false,
         when: job[:when] || 'on_success',
-        environment: job[:environment],
+        environment: job[:environment_name],
         yaml_variables: yaml_variables(name),
         options: {
-          image: job[:image] || @image,
-          services: job[:services] || @services,
+          image: job[:image],
+          services: job[:services],
           artifacts: job[:artifacts],
-          cache: job[:cache] || @cache,
+          cache: job[:cache],
           dependencies: job[:dependencies],
-          after_script: job[:after_script] || @after_script,
+          after_script: job[:after_script],
+          environment: job[:environment],
         }.compact
       }
     end
 
+    def self.validation_message(content)
+      return 'Please provide content of .gitlab-ci.yml' if content.blank?
+
+      begin
+        Ci::GitlabCiYamlProcessor.new(content)
+        nil
+      rescue ValidationError, Psych::SyntaxError => e
+        e.message
+      end
+    end
+
     private
 
     def initial_parsing
@@ -102,6 +109,7 @@ module Ci
 
         validate_job_stage!(name, job)
         validate_job_dependencies!(name, job)
+        validate_job_environment!(name, job)
       end
     end
 
@@ -143,6 +151,35 @@ module Ci
       end
     end
 
+    def validate_job_environment!(name, job)
+      return unless job[:environment]
+      return unless job[:environment].is_a?(Hash)
+
+      environment = job[:environment]
+      validate_on_stop_job!(name, environment, environment[:on_stop])
+    end
+
+    def validate_on_stop_job!(name, environment, on_stop)
+      return unless on_stop
+
+      on_stop_job = @jobs[on_stop.to_sym]
+      unless on_stop_job
+        raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
+      end
+
+      unless on_stop_job[:environment]
+        raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
+      end
+
+      unless on_stop_job[:environment][:name] == environment[:name]
+        raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
+      end
+
+      unless on_stop_job[:environment][:action] == 'stop'
+        raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
+      end
+    end
+
     def process?(only_params, except_params, ref, tag, trigger_request)
       if only_params.present?
         return false unless matching?(only_params, ref, tag, trigger_request)
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
new file mode 100644
index 0000000000000000000000000000000000000000..997377abc55fd6ff858298319dd36a342469994f
--- /dev/null
+++ b/lib/ci/mask_secret.rb
@@ -0,0 +1,10 @@
+module Ci::MaskSecret
+  class << self
+    def mask!(value, token)
+      return value unless value.present? && token.present?
+
+      value.gsub!(token, 'x' * token.length)
+      value
+    end
+  end
+end
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
deleted file mode 100644
index 2a87c91db5e22de5fc3363ed60e94512f6aca6ff..0000000000000000000000000000000000000000
--- a/lib/ci/version_info.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-class VersionInfo
-  include Comparable
-
-  attr_reader :major, :minor, :patch
-
-  def self.parse(str)
-    if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
-      VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
-    else
-      VersionInfo.new
-    end
-  end
-
-  def initialize(major = 0, minor = 0, patch = 0)
-    @major = major
-    @minor = minor
-    @patch = patch
-  end
-
-  def <=>(other)
-    return unless other.is_a? VersionInfo
-    return unless valid? && other.valid?
-
-    if other.major < @major
-      1
-    elsif @major < other.major
-      -1
-    elsif other.minor < @minor
-      1
-    elsif @minor < other.minor
-      -1
-    elsif other.patch < @patch
-      1
-    elsif @patch < other.patch
-      -1
-    else
-      0
-    end
-  end
-
-  def to_s
-    if valid?
-      "%d.%d.%d" % [@major, @minor, @patch]
-    else
-      "Unknown"
-    end
-  end
-
-  def valid?
-    @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
-  end
-end
diff --git a/lib/constraints/constrainer_helper.rb b/lib/constraints/constrainer_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab07a6793d90d5b3a6b8067a46b8b8d80a7972f1
--- /dev/null
+++ b/lib/constraints/constrainer_helper.rb
@@ -0,0 +1,15 @@
+module ConstrainerHelper
+  def extract_resource_path(path)
+    id = path.dup
+    id.sub!(/\A#{relative_url_root}/, '') if relative_url_root
+    id.sub(/\A\/+/, '').sub(/\/+\z/, '').sub(/.atom\z/, '')
+  end
+
+  private
+
+  def relative_url_root
+    if defined?(Gitlab::Application.config.relative_url_root)
+      Gitlab::Application.config.relative_url_root
+    end
+  end
+end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2af6e1a11c861c5f1a843f360ef0fddf96cdaa73
--- /dev/null
+++ b/lib/constraints/group_url_constrainer.rb
@@ -0,0 +1,15 @@
+require_relative 'constrainer_helper'
+
+class GroupUrlConstrainer
+  include ConstrainerHelper
+
+  def matches?(request)
+    id = extract_resource_path(request.path)
+
+    if id =~ Gitlab::Regex.namespace_regex
+      Group.find_by(path: id).present?
+    else
+      false
+    end
+  end
+end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4d722ad5af248af5c4b62504d7fbd628a81ad052
--- /dev/null
+++ b/lib/constraints/user_url_constrainer.rb
@@ -0,0 +1,15 @@
+require_relative 'constrainer_helper'
+
+class UserUrlConstrainer
+  include ConstrainerHelper
+
+  def matches?(request)
+    id = extract_resource_path(request.path)
+
+    if id =~ Gitlab::Regex.namespace_regex
+      User.find_by('lower(username) = ?', id.downcase).present?
+    else
+      false
+    end
+  end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 668d2fa41b3064f2fe16310a826e1e3cce268671..21f6a9a762b43164920f4eef663a7f23342517d5 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -2,8 +2,8 @@ class EventFilter
   attr_accessor :params
 
   class << self
-    def default_filter
-      %w{ push issues merge_requests team}
+    def all
+      'all'
     end
 
     def push
@@ -35,18 +35,28 @@ class EventFilter
     return events unless params.present?
 
     filter = params.dup
-
     actions = []
-    actions << Event::PUSHED if filter.include? 'push'
-    actions << Event::MERGED if filter.include? 'merged'
 
-    if filter.include? 'team'
-      actions << Event::JOINED
-      actions << Event::LEFT
+    case filter
+    when EventFilter.push
+      actions = [Event::PUSHED]
+    when EventFilter.merged
+      actions = [Event::MERGED]
+    when EventFilter.comments
+      actions = [Event::COMMENTED]
+    when EventFilter.team
+      actions = [Event::JOINED, Event::LEFT, Event::EXPIRED]
+    when EventFilter.all
+      actions = [
+        Event::PUSHED,
+        Event::MERGED,
+        Event::COMMENTED,
+        Event::JOINED,
+        Event::LEFT,
+        Event::EXPIRED
+      ]
     end
 
-    actions << Event::COMMENTED if filter.include? 'comments'
-
     events.where(action: actions)
   end
 
diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b1533d0d321e49c710dcdb0289552d2cd2b4516
--- /dev/null
+++ b/lib/expand_variables.rb
@@ -0,0 +1,17 @@
+module ExpandVariables
+  class << self
+    def expand(value, variables)
+      # Convert hash array to variables
+      if variables.is_a?(Array)
+        variables = variables.reduce({}) do |hash, variable|
+          hash[variable[:key]] = variable[:value]
+          hash
+        end
+      end
+
+      value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
+        variables[$1 || $2]
+      end
+    end
+  end
+end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 84688f6646eb849c0e84ee097491404249d63c42..82551f1f2223767fea1b16ab48e4553fb8dab363 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -52,8 +52,7 @@ module ExtractsPath
       # Append a trailing slash if we only get a ref and no file path
       id += '/' unless id.ends_with?('/')
 
-      valid_refs = @project.repository.ref_names
-      valid_refs.select! { |v| id.start_with?("#{v}/") }
+      valid_refs = ref_names.select { |v| id.start_with?("#{v}/") }
 
       if valid_refs.length == 0
         # No exact ref match, so just try our best
@@ -74,6 +73,19 @@ module ExtractsPath
     pair
   end
 
+  # If we have an ID of 'foo.atom', and the controller provides Atom and HTML
+  # formats, then we have to check if the request was for the Atom version of
+  # the ID without the '.atom' suffix, or the HTML version of the ID including
+  # the suffix. We only check this if the version including the suffix doesn't
+  # match, so it is possible to create a branch which has an unroutable Atom
+  # feed.
+  def extract_ref_without_atom(id)
+    id_without_atom = id.sub(/\.atom$/, '')
+    valid_refs = ref_names.select { |v| "#{id_without_atom}/".start_with?("#{v}/") }
+
+    valid_refs.max_by(&:length)
+  end
+
   # Assigns common instance variables for views working with Git tree-ish objects
   #
   # Assignments are:
@@ -86,21 +98,29 @@ module ExtractsPath
   # If the :id parameter appears to be requesting a specific response format,
   # that will be handled as well.
   #
+  # If there is no path and the ref doesn't exist in the repo, try to resolve
+  # the ref without an '.atom' suffix. If _that_ ref is found, set the request's
+  # format to Atom manually.
+  #
   # Automatically renders `not_found!` if a valid tree path could not be
   # resolved (e.g., when a user inserts an invalid path or ref).
   def assign_ref_vars
     # assign allowed options
-    allowed_options = ["filter_ref", "extended_sha1"]
+    allowed_options = ["filter_ref"]
     @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
     @options = HashWithIndifferentAccess.new(@options)
 
-    @id = Addressable::URI.normalize_component(get_id)
+    @id = get_id
     @ref, @path = extract_ref(@id)
     @repo = @project.repository
-    if @options[:extended_sha1].blank?
+
+    @commit = @repo.commit(@ref)
+
+    if @path.empty? && !@commit && @id.ends_with?('.atom')
+      @id = @ref = extract_ref_without_atom(@id)
       @commit = @repo.commit(@ref)
-    else
-      @commit = @repo.commit(@options[:extended_sha1])
+
+      request.format = :atom if @commit
     end
 
     raise InvalidPathError unless @commit
@@ -119,9 +139,16 @@ module ExtractsPath
 
   private
 
+  # overriden in subclasses, do not remove
   def get_id
     id = params[:id] || params[:ref]
     id += "/" + params[:path] unless params[:path].blank?
     id
   end
+
+  def ref_names
+    return [] unless @project
+
+    @ref_names ||= @project.repository.ref_names
+  end
 end
diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7cb4bccb23c4c1b037da3b9b7093e3a22a6ce774
--- /dev/null
+++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
@@ -0,0 +1,15 @@
+require 'rails/generators'
+
+module Rails
+  class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
+    def create_migration_file
+      timestamp = Time.now.strftime('%Y%m%d%H%I%S')
+
+      template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
+    end
+
+    def migration_class_name
+      file_name.camelize
+    end
+  end
+end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index a533bac26925ff589a89a4c312df8040769e28bd..9b484a2ecfd9ddfbb3a31a9f36357cf6fee801ea 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -53,6 +53,10 @@ module Gitlab
         }
       end
 
+      def sym_options_with_owner
+        sym_options.merge(owner: OWNER)
+      end
+
       def protection_options
         {
           "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index db1704af75ebb26077ce156ea508f78520f8b423..aca5d0020cf5874313bcbac619dee3501ae77136 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,22 +1,22 @@
 module Gitlab
   module Auth
-    Result = Struct.new(:user, :type)
+    class MissingPersonalTokenError < StandardError; end
 
     class << self
       def find_for_git_client(login, password, project:, ip:)
         raise "Must provide an IP for rate limiting" if ip.nil?
 
-        result = Result.new
+        result =
+          service_request_check(login, password, project) ||
+          build_access_token_check(login, password) ||
+          user_with_password_for_git(login, password) ||
+          oauth_access_token_check(login, password) ||
+          lfs_token_check(login, password) ||
+          personal_access_token_check(login, password) ||
+          Gitlab::Auth::Result.new
 
-        if valid_ci_request?(login, password, project)
-          result.type = :ci
-        elsif result.user = find_with_user_password(login, password)
-          result.type = :gitlab_or_ldap
-        elsif result.user = oauth_access_token_check(login, password)
-          result.type = :oauth
-        end
+        rate_limit!(ip, success: result.success?, login: login)
 
-        rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
         result
       end
 
@@ -58,30 +58,117 @@ module Gitlab
 
       private
 
-      def valid_ci_request?(login, password, project)
+      def service_request_check(login, password, project)
         matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
 
-        return false unless project && matched_login.present?
+        return unless project && matched_login.present?
 
         underscored_service = matched_login['service'].underscore
 
-        if underscored_service == 'gitlab_ci'
-          project && project.valid_build_token?(password)
-        elsif Service.available_services_names.include?(underscored_service)
+        if Service.available_services_names.include?(underscored_service)
           # We treat underscored_service as a trusted input because it is included
           # in the Service.available_services_names whitelist.
           service = project.public_send("#{underscored_service}_service")
 
-          service && service.activated? && service.valid_token?(password)
+          if service && service.activated? && service.valid_token?(password)
+            Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
+          end
         end
       end
 
+      def user_with_password_for_git(login, password)
+        user = find_with_user_password(login, password)
+        return unless user
+
+        raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+
+        Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
+      end
+
       def oauth_access_token_check(login, password)
         if login == "oauth2" && password.present?
           token = Doorkeeper::AccessToken.by_token(password)
-          token && token.accessible? && User.find_by(id: token.resource_owner_id)
+          if token && token.accessible?
+            user = User.find_by(id: token.resource_owner_id)
+            Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
+          end
+        end
+      end
+
+      def personal_access_token_check(login, password)
+        if login && password
+          user = User.find_by_personal_access_token(password)
+          validation = User.by_login(login)
+          Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+        end
+      end
+
+      def lfs_token_check(login, password)
+        deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
+
+        actor =
+          if deploy_key_matches
+            DeployKey.find(deploy_key_matches[1])
+          else
+            User.by_login(login)
+          end
+
+        return unless actor
+
+        token_handler = Gitlab::LfsToken.new(actor)
+
+        authentication_abilities =
+          if token_handler.user?
+            full_authentication_abilities
+          else
+            read_authentication_abilities
+          end
+
+        Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+      end
+
+      def build_access_token_check(login, password)
+        return unless login == 'gitlab-ci-token'
+        return unless password
+
+        build = ::Ci::Build.running.find_by_token(password)
+        return unless build
+        return unless build.project.builds_enabled?
+
+        if build.user
+          # If user is assigned to build, use restricted credentials of user
+          Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
+        else
+          # Otherwise use generic CI credentials (backward compatibility)
+          Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
         end
       end
+
+      public
+
+      def build_authentication_abilities
+        [
+          :read_project,
+          :build_download_code,
+          :build_read_container_image,
+          :build_create_container_image
+        ]
+      end
+
+      def read_authentication_abilities
+        [
+          :read_project,
+          :download_code,
+          :read_container_image
+        ]
+      end
+
+      def full_authentication_abilities
+        read_authentication_abilities + [
+          :push_code,
+          :create_container_image
+        ]
+      end
     end
   end
 end
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6be7f690676b0169dfb1f27e5c12032d5ece35bc
--- /dev/null
+++ b/lib/gitlab/auth/result.rb
@@ -0,0 +1,21 @@
+module Gitlab
+  module Auth
+    Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
+      def ci?(for_project)
+        type == :ci &&
+          project &&
+          project == for_project
+      end
+
+      def lfs_deploy_token?(for_project)
+        type == :lfs_deploy_token &&
+          actor &&
+          actor.projects.include?(for_project)
+      end
+
+      def success?
+        actor.present? || type == :ci
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 839a4fa30d5de358fc17b7f1ac53ec712a424f02..82e194c1af12f32943e9fe67478c4583161d39dd 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -6,16 +6,56 @@ module Gitlab
 
     KeyAdder = Struct.new(:io) do
       def add_key(id, key)
-        key.gsub!(/[[:space:]]+/, ' ').strip!
+        key = Gitlab::Shell.strip_key(key)
+        # Newline and tab are part of the 'protocol' used to transmit id+key to the other end
+        if key.include?("\t") || key.include?("\n")
+          raise Error.new("Invalid key: #{key.inspect}")
+        end
+
         io.puts("#{id}\t#{key}")
       end
     end
 
     class << self
+      def secret_token
+        @secret_token ||= begin
+          File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+        end
+      end
+
+      def ensure_secret_token!
+        return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret'))
+
+        generate_and_link_secret_token
+      end
+
       def version_required
         @version_required ||= File.read(Rails.root.
                                         join('GITLAB_SHELL_VERSION')).strip
       end
+
+      def strip_key(key)
+        key.split(/ /)[0, 2].join(' ')
+      end
+
+      private
+
+      # Create (if necessary) and link the secret token file
+      def generate_and_link_secret_token
+        secret_file = Gitlab.config.gitlab_shell.secret_file
+        shell_path = Gitlab.config.gitlab_shell.path
+
+        unless File.size?(secret_file)
+          # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
+          @secret_token = SecureRandom.hex(16)
+          File.write(secret_file, @secret_token)
+        end
+
+        link_path = File.join(shell_path, '.gitlab_shell_secret')
+        if File.exist?(shell_path) && !File.exist?(link_path)
+          FileUtils.symlink(secret_file, link_path)
+        end
+      end
     end
 
     # Init new repository
@@ -87,19 +127,6 @@ module Gitlab
                                    'rm-project', storage, "#{name}.git"])
     end
 
-    # Gc repository
-    #
-    # storage - project storage path
-    # path - project path with namespace
-    #
-    # Ex.
-    #   gc("/path/to/storage", "gitlab/gitlab-ci")
-    #
-    def gc(storage, path)
-      Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc',
-                                   storage, "#{path}.git"])
-    end
-
     # Add new key to gitlab-shell
     #
     # Ex.
@@ -107,7 +134,7 @@ module Gitlab
     #
     def add_key(key_id, key_content)
       Gitlab::Utils.system_silent([gitlab_shell_keys_path,
-                                   'add-key', key_id, key_content])
+                                   'add-key', key_id, self.class.strip_key(key_content)])
     end
 
     # Batch-add keys to authorized_keys
@@ -192,21 +219,6 @@ module Gitlab
       File.exist?(full_path(storage, dir_name))
     end
 
-    # Create (if necessary) and link the secret token file
-    def generate_and_link_secret_token
-      secret_file = Gitlab.config.gitlab_shell.secret_file
-      unless File.exist? secret_file
-        # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
-        token = SecureRandom.hex(16)
-        File.write(secret_file, token)
-      end
-
-      link_path = File.join(gitlab_shell_path, '.gitlab_shell_secret')
-      if File.exist?(gitlab_shell_path) && !File.exist?(link_path)
-        FileUtils.symlink(secret_file, link_path)
-      end
-    end
-
     protected
 
     def gitlab_shell_path
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 3d56ea3e47a390b6b579121ecf3da12756650582..9a0482306b7ebda075cda06589ddc1699ab55673 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -12,10 +12,7 @@ module Gitlab
           @ref = ref
           @job = job
 
-          @pipeline = @project.pipelines
-            .where(ref: @ref)
-            .where(sha: @project.commit(@ref).try(:sha))
-            .first
+          @pipeline = @project.pipelines.latest_successful_for(@ref)
         end
 
         def entity
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 7beaecd1cf065637cf09a672e8ac843bce4bfbae..f4b5097adb1f4c18d915ed781378c3ff157d22f3 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -21,7 +21,7 @@ module Gitlab
 
       private
 
-      def gl_user_id(project, bitbucket_id)
+      def gitlab_user_id(project, bitbucket_id)
         if bitbucket_id
           user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
           (user && user.id) || project.creator_id
@@ -74,7 +74,7 @@ module Gitlab
             description: body,
             title: issue["title"],
             state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
-            author_id: gl_user_id(project, reporter)
+            author_id: gitlab_user_id(project, reporter)
           )
         end
       rescue ActiveRecord::RecordInvalid => e
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 4b32eb966aa050c48c3afe1b6f83a276f0f372ad..cb1065223d4d65b9de3602a5dbc8df63a977137f 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -23,6 +23,7 @@ module Gitlab
       protected
 
       def protected_branch_checks
+        return unless @branch_name
         return unless project.protected_branch?(@branch_name)
 
         if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index ae82c0db3f1ce40f0bc4de001d4ce0e42e5e8d5c..bbfa6cf7d05ab66d2061315f1667ef7e978007bf 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -14,7 +14,7 @@ module Gitlab
         @config = Loader.new(config).load!
 
         @global = Node::Global.new(@config)
-        @global.process!
+        @global.compose!
       end
 
       def valid?
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
index 2de82d40c9dad753c8e1f4c850ec3b0255d4ab39..6b7ab2fdaf266b8b3497414c8ff5df6d2cb31aa6 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -23,9 +23,9 @@ module Gitlab
             end
           end
 
-          private
+          def compose!(deps = nil)
+            return unless valid?
 
-          def compose!
             self.class.nodes.each do |key, factory|
               factory
                 .value(@config[key])
@@ -33,6 +33,12 @@ module Gitlab
 
               @entries[key] = factory.create!
             end
+
+            yield if block_given?
+
+            @entries.each_value do |entry|
+              entry.compose!(deps)
+            end
           end
 
           class_methods do
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 0c782c422b583d706b4ab33a9764bc2969ca1ef5..8717eabf81eb28b7c2b42ecf542c37aa6f9491ed 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -20,11 +20,14 @@ module Gitlab
             @validator.validate(:new)
           end
 
-          def process!
+          def [](key)
+            @entries[key] || Node::Undefined.new
+          end
+
+          def compose!(deps = nil)
             return unless valid?
 
-            compose!
-            descendants.each(&:process!)
+            yield if block_given?
           end
 
           def leaf?
@@ -73,11 +76,6 @@ module Gitlab
           def self.validator
             Validator
           end
-
-          private
-
-          def compose!
-          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a95ef43628e84081907959e7a44fcce9d36089a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/environment.rb
@@ -0,0 +1,82 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        ##
+        # Entry that represents an environment.
+        #
+        class Environment < Entry
+          include Validatable
+
+          ALLOWED_KEYS = %i[name url action on_stop]
+
+          validations do
+            validate do
+              unless hash? || string?
+                errors.add(:config, 'should be a hash or a string')
+              end
+            end
+
+            validates :name, presence: true
+            validates :name,
+              type: {
+                with: String,
+                message: Gitlab::Regex.environment_name_regex_message }
+
+            validates :name,
+              format: {
+                with: Gitlab::Regex.environment_name_regex,
+                message: Gitlab::Regex.environment_name_regex_message }
+
+            with_options if: :hash? do
+              validates :config, allowed_keys: ALLOWED_KEYS
+
+              validates :url,
+                        length: { maximum: 255 },
+                        addressable_url: true,
+                        allow_nil: true
+
+              validates :action,
+                        inclusion: { in: %w[start stop], message: 'should be start or stop' },
+                        allow_nil: true
+
+              validates :on_stop, type: String, allow_nil: true
+            end
+          end
+
+          def hash?
+            @config.is_a?(Hash)
+          end
+
+          def string?
+            @config.is_a?(String)
+          end
+
+          def name
+            value[:name]
+          end
+
+          def url
+            value[:url]
+          end
+
+          def action
+            value[:action] || 'start'
+          end
+
+          def on_stop
+            value[:on_stop]
+          end
+
+          def value
+            case @config
+            when String then { name: @config, action: 'start' }
+            when Hash then @config
+            else {}
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
index 707b052e6a8f15f8b15cbd63e0c18235ad9b7428..5387f29ad5946aa8f90cb1703caa2b9209cadffd 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -37,8 +37,8 @@ module Gitlab
             # See issue #18775.
             #
             if @value.nil?
-              Node::Undefined.new(
-                fabricate_undefined
+              Node::Unspecified.new(
+                fabricate_unspecified
               )
             else
               fabricate(@node, @value)
@@ -47,13 +47,13 @@ module Gitlab
 
           private
 
-          def fabricate_undefined
+          def fabricate_unspecified
             ##
             # If node has a default value we fabricate concrete node
             # with default value.
             #
             if @node.default.nil?
-              fabricate(Node::Null)
+              fabricate(Node::Undefined)
             else
               fabricate(@node, @node.default)
             end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
index ccd539fb0037cb43d2d39f70e2f5dd357ae19334..2a2943c92886ce205053e7096a356331b938518b 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -36,15 +36,15 @@ module Gitlab
           helpers :before_script, :image, :services, :after_script,
                   :variables, :stages, :types, :cache, :jobs
 
-          private
-
-          def compose!
-            super
-
-            compose_jobs!
-            compose_deprecated_entries!
+          def compose!(_deps = nil)
+            super(self) do
+              compose_jobs!
+              compose_deprecated_entries!
+            end
           end
 
+          private
+
           def compose_jobs!
             factory = Node::Factory.new(Node::Jobs)
               .value(@config.except(*self.class.nodes.keys))
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb
similarity index 82%
rename from lib/gitlab/ci/config/node/hidden_job.rb
rename to lib/gitlab/ci/config/node/hidden.rb
index 073044b66f89c66ae593d1042b22214b6d901bfc..fe4ee8a7fc6f33bfa5ff8951238d9e2f2edcd900 100644
--- a/lib/gitlab/ci/config/node/hidden_job.rb
+++ b/lib/gitlab/ci/config/node/hidden.rb
@@ -5,11 +5,10 @@ module Gitlab
         ##
         # Entry that represents a hidden CI/CD job.
         #
-        class HiddenJob < Entry
+        class Hidden < Entry
           include Validatable
 
           validations do
-            validates :config, type: Hash
             validates :config, presence: true
           end
 
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
index e84737acbb98e2f70d96a6895bb9a908949e9e1a..603334d6793562c12866c4fceb627bc8b2c7aeab 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -13,7 +13,7 @@ module Gitlab
                             type stage when artifacts cache dependencies before_script
                             after_script variables environment]
 
-          attributes :tags, :allow_failure, :when, :environment, :dependencies
+          attributes :tags, :allow_failure, :when, :dependencies
 
           validations do
             validates :config, allowed_keys: ALLOWED_KEYS
@@ -29,58 +29,65 @@ module Gitlab
                 inclusion: { in: %w[on_success on_failure always manual],
                              message: 'should be on_success, on_failure, ' \
                                       'always or manual' }
-              validates :environment,
-                type: {
-                  with: String,
-                  message: Gitlab::Regex.environment_name_regex_message }
-              validates :environment,
-                format: {
-                  with: Gitlab::Regex.environment_name_regex,
-                  message: Gitlab::Regex.environment_name_regex_message }
 
               validates :dependencies, array_of_strings: true
             end
           end
 
-          node :before_script, Script,
+          node :before_script, Node::Script,
             description: 'Global before script overridden in this job.'
 
-          node :script, Commands,
+          node :script, Node::Commands,
             description: 'Commands that will be executed in this job.'
 
-          node :stage, Stage,
+          node :stage, Node::Stage,
             description: 'Pipeline stage this job will be executed into.'
 
-          node :type, Stage,
+          node :type, Node::Stage,
             description: 'Deprecated: stage this job will be executed into.'
 
-          node :after_script, Script,
+          node :after_script, Node::Script,
             description: 'Commands that will be executed when finishing job.'
 
-          node :cache, Cache,
+          node :cache, Node::Cache,
             description: 'Cache definition for this job.'
 
-          node :image, Image,
+          node :image, Node::Image,
             description: 'Image that will be used to execute this job.'
 
-          node :services, Services,
+          node :services, Node::Services,
             description: 'Services that will be used to execute this job.'
 
-          node :only, Trigger,
+          node :only, Node::Trigger,
             description: 'Refs policy this job will be executed for.'
 
-          node :except, Trigger,
+          node :except, Node::Trigger,
             description: 'Refs policy this job will be executed for.'
 
-          node :variables, Variables,
+          node :variables, Node::Variables,
             description: 'Environment variables available for this job.'
 
-          node :artifacts, Artifacts,
+          node :artifacts, Node::Artifacts,
             description: 'Artifacts configuration for this job.'
 
+          node :environment, Node::Environment,
+               description: 'Environment configuration for this job.'
+
           helpers :before_script, :script, :stage, :type, :after_script,
                   :cache, :image, :services, :only, :except, :variables,
-                  :artifacts
+                  :artifacts, :commands, :environment
+
+          def compose!(deps = nil)
+            super do
+              if type_defined? && !stage_defined?
+                @entries[:stage] = @entries[:type]
+              end
+
+              @entries.delete(:type)
+            end
+
+            inherit!(deps)
+          end
 
           def name
             @metadata[:name]
@@ -90,12 +97,30 @@ module Gitlab
             @config.merge(to_hash.compact)
           end
 
+          def commands
+            (before_script_value.to_a + script_value.to_a).join("\n")
+          end
+
           private
 
+          def inherit!(deps)
+            return unless deps
+
+            self.class.nodes.each_key do |key|
+              global_entry = deps[key]
+              job_entry = @entries[key]
+
+              if global_entry.specified? && !job_entry.specified?
+                @entries[key] = global_entry
+              end
+            end
+          end
+
           def to_hash
             { name: name,
               before_script: before_script,
               script: script,
+              commands: commands,
               image: image,
               services: services,
               stage: stage,
@@ -103,19 +128,11 @@ module Gitlab
               only: only,
               except: except,
               variables: variables_defined? ? variables : nil,
+              environment: environment_defined? ? environment : nil,
+              environment_name: environment_defined? ? environment[:name] : nil,
               artifacts: artifacts,
               after_script: after_script }
           end
-
-          def compose!
-            super
-
-            if type_defined? && !stage_defined?
-              @entries[:stage] = @entries[:type]
-            end
-
-            @entries.delete(:type)
-          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index 51683c82ceb7565f742b5063fd2163ac5bc8c654..d10e80d1a7d3482fa613000fba75217bfe11e5e4 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -26,19 +26,23 @@ module Gitlab
             name.to_s.start_with?('.')
           end
 
-          private
-
-          def compose!
-            @config.each do |name, config|
-              node = hidden?(name) ? Node::HiddenJob : Node::Job
-
-              factory = Node::Factory.new(node)
-                .value(config || {})
-                .metadata(name: name)
-                .with(key: name, parent: self,
-                      description: "#{name} job definition.")
+          def compose!(deps = nil)
+            super do
+              @config.each do |name, config|
+                node = hidden?(name) ? Node::Hidden : Node::Job
+
+                factory = Node::Factory.new(node)
+                  .value(config || {})
+                  .metadata(name: name)
+                  .with(key: name, parent: self,
+                        description: "#{name} job definition.")
+
+                @entries[name] = factory.create!
+              end
 
-              @entries[name] = factory.create!
+              @entries.each_value do |entry|
+                entry.compose!(deps)
+              end
             end
           end
         end
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
deleted file mode 100644
index 88a5f53f13c1a046e8754acad7b4052f992b40cc..0000000000000000000000000000000000000000
--- a/lib/gitlab/ci/config/node/null.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Gitlab
-  module Ci
-    class Config
-      module Node
-        ##
-        # This class represents an undefined node.
-        #
-        # Implements the Null Object pattern.
-        #
-        class Null < Entry
-          def value
-            nil
-          end
-
-          def valid?
-            true
-          end
-
-          def errors
-            []
-          end
-
-          def specified?
-            false
-          end
-
-          def relevant?
-            false
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb
index 45fef8c3ae55204c3d0a5a87b92de655e9d4cebe..33e78023539d0270d38e27bf79b72d2a54a4e539 100644
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ b/lib/gitlab/ci/config/node/undefined.rb
@@ -3,15 +3,34 @@ module Gitlab
     class Config
       module Node
         ##
-        # This class represents an unspecified entry node.
+        # This class represents an undefined node.
         #
-        # It decorates original entry adding method that indicates it is
-        # unspecified.
+        # Implements the Null Object pattern.
         #
-        class Undefined < SimpleDelegator
+        class Undefined < Entry
+          def initialize(*)
+            super(nil)
+          end
+
+          def value
+            nil
+          end
+
+          def valid?
+            true
+          end
+
+          def errors
+            []
+          end
+
           def specified?
             false
           end
+
+          def relevant?
+            false
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/node/unspecified.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7d1f6131b8722d5228723eec21ecf9194e9bb79
--- /dev/null
+++ b/lib/gitlab/ci/config/node/unspecified.rb
@@ -0,0 +1,19 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        ##
+        # This class represents an unspecified entry node.
+        #
+        # It decorates original entry adding method that indicates it is
+        # unspecified.
+        #
+        class Unspecified < SimpleDelegator
+          def specified?
+            false
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a210e76acaa41baa7658853232b208149b14b311
--- /dev/null
+++ b/lib/gitlab/ci/pipeline_duration.rb
@@ -0,0 +1,141 @@
+module Gitlab
+  module Ci
+    # # Introduction - total running time
+    #
+    # The problem this module is trying to solve is finding the total running
+    # time amongst all the jobs, excluding retries and pending (queue) time.
+    # We could reduce this problem down to finding the union of periods.
+    #
+    # So each job would be represented as a `Period`, which consists of
+    # `Period#first` as when the job started and `Period#last` as when the
+    # job was finished. A simple example here would be:
+    #
+    # * A (1, 3)
+    # * B (2, 4)
+    # * C (6, 7)
+    #
+    # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
+    # C begins from 6, and ends to 7. Visually it could be viewed as:
+    #
+    #     0  1  2  3  4  5  6  7
+    #        AAAAAAA
+    #           BBBBBBB
+    #                       CCCC
+    #
+    # The union of A, B, and C would be (1, 4) and (6, 7), therefore the
+    # total running time should be:
+    #
+    #     (4 - 1) + (7 - 6) => 4
+    #
+    # # The Algorithm
+    #
+    # The algorithm used here for union would be described as follow.
+    # First we make sure that all periods are sorted by `Period#first`.
+    # Then we try to merge periods by iterating through the first period
+    # to the last period. The goal would be merging all overlapped periods
+    # so that in the end all the periods are discrete. When all periods
+    # are discrete, we're free to just sum all the periods to get real
+    # running time.
+    #
+    # Here we begin from A, and compare it to B. We could find that
+    # before A ends, B already started. That is `B.first <= A.last`
+    # that is `2 <= 3` which means A and B are overlapping!
+    #
+    # When we found that two periods are overlapping, we would need to merge
+    # them into a new period and disregard the old periods. To make a new
+    # period, we take `A.first` as the new first because remember? we sorted
+    # them, so `A.first` must be smaller or equal to `B.first`. And we take
+    # `[A.last, B.last].max` as the new last because we want whoever ended
+    # later. This could be broken into two cases:
+    #
+    #     0  1  2  3  4
+    #        AAAAAAA
+    #           BBBBBBB
+    #
+    # Or:
+    #
+    #     0  1  2  3  4
+    #        AAAAAAAAAA
+    #           BBBB
+    #
+    # So that we need to take whoever ends later. Back to our example,
+    # after merging and discard A and B it could be visually viewed as:
+    #
+    #     0  1  2  3  4  5  6  7
+    #        DDDDDDDDDD
+    #                       CCCC
+    #
+    # Now we could go on and compare the newly created D and the old C.
+    # We could figure out that D and C are not overlapping by checking
+    # `C.first <= D.last` is `false`. Therefore we need to keep both C
+    # and D. The example would end here because there are no more jobs.
+    #
+    # After having the union of all periods, we just need to sum the length
+    # of all periods to get total time.
+    #
+    #     (4 - 1) + (7 - 6) => 4
+    #
+    # That is 4 is the answer in the example.
+    module PipelineDuration
+      extend self
+
+      Period = Struct.new(:first, :last) do
+        def duration
+          last - first
+        end
+      end
+
+      def from_pipeline(pipeline)
+        status = %w[success failed running canceled]
+        builds = pipeline.builds.latest.
+          where(status: status).where.not(started_at: nil).order(:started_at)
+
+        from_builds(builds)
+      end
+
+      def from_builds(builds)
+        now = Time.now
+
+        periods = builds.map do |b|
+          Period.new(b.started_at, b.finished_at || now)
+        end
+
+        from_periods(periods)
+      end
+
+      # periods should be sorted by `first`
+      def from_periods(periods)
+        process_duration(process_periods(periods))
+      end
+
+      private
+
+      def process_periods(periods)
+        return periods if periods.empty?
+
+        periods.drop(1).inject([periods.first]) do |result, current|
+          previous = result.last
+
+          if overlap?(previous, current)
+            result[-1] = merge(previous, current)
+            result
+          else
+            result << current
+          end
+        end
+      end
+
+      def overlap?(previous, current)
+        current.first <= previous.last
+      end
+
+      def merge(previous, current)
+        Period.new(previous.first, [previous.last, current.last].max)
+      end
+
+      def process_duration(periods)
+        periods.sum(&:duration)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37e51536e8fc1b21ddd94a2f9712f801c18e3f04
--- /dev/null
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -0,0 +1,49 @@
+module Gitlab
+  module Ci
+    # This was inspired from: http://stackoverflow.com/a/10219411/1520132
+    class TraceReader
+      BUFFER_SIZE = 4096
+
+      attr_accessor :path, :buffer_size
+
+      def initialize(new_path, buffer_size: BUFFER_SIZE)
+        self.path = new_path
+        self.buffer_size = Integer(buffer_size)
+      end
+
+      def read(last_lines: nil)
+        if last_lines
+          read_last_lines(last_lines)
+        else
+          File.read(path)
+        end
+      end
+
+      def read_last_lines(max_lines)
+        File.open(path) do |file|
+          chunks = []
+          pos = lines = 0
+          max = file.size
+
+          # We want an extra line to make sure fist line has full contents
+          while lines <= max_lines && pos < max
+            pos += buffer_size
+
+            buf = if pos <= max
+                    file.seek(-pos, IO::SEEK_END)
+                    file.read(buffer_size)
+                  else # Reached the head, read only left
+                    file.seek(0)
+                    file.read(buffer_size - (pos - max))
+                  end
+
+            lines += buf.count("\n")
+            chunks.unshift(buf)
+          end
+
+          chunks.join.lines.last(max_lines).join
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c843315782dc1eb167c4dfecda4f99024c75af1f
--- /dev/null
+++ b/lib/gitlab/conflict/file.rb
@@ -0,0 +1,245 @@
+module Gitlab
+  module Conflict
+    class File
+      include Gitlab::Routing.url_helpers
+      include IconsHelper
+
+      class MissingResolution < ResolutionError
+      end
+
+      CONTEXT_LINES = 3
+
+      attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
+
+      def initialize(merge_file_result, conflict, merge_request:)
+        @merge_file_result = merge_file_result
+        @their_path = conflict[:theirs][:path]
+        @our_path = conflict[:ours][:path]
+        @our_mode = conflict[:ours][:mode]
+        @merge_request = merge_request
+        @repository = merge_request.project.repository
+        @match_line_headers = {}
+      end
+
+      def content
+        merge_file_result[:data]
+      end
+
+      def our_blob
+        @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
+      end
+
+      def type
+        lines unless @type
+
+        @type.inquiry
+      end
+
+      # Array of Gitlab::Diff::Line objects
+      def lines
+        return @lines if defined?(@lines)
+
+        begin
+          @type = 'text'
+          @lines = Gitlab::Conflict::Parser.new.parse(content,
+                                                      our_path: our_path,
+                                                      their_path: their_path,
+                                                      parent_file: self)
+        rescue Gitlab::Conflict::Parser::ParserError
+          @type = 'text-editor'
+          @lines = nil
+        end
+      end
+
+      def resolve_lines(resolution)
+        section_id = nil
+
+        lines.map do |line|
+          unless line.type
+            section_id = nil
+            next line
+          end
+
+          section_id ||= line_code(line)
+
+          case resolution[section_id]
+          when 'head'
+            next unless line.type == 'new'
+          when 'origin'
+            next unless line.type == 'old'
+          else
+            raise MissingResolution, "Missing resolution for section ID: #{section_id}"
+          end
+
+          line
+        end.compact
+      end
+
+      def resolve_content(resolution)
+        if resolution == content
+          raise MissingResolution, "Resolved content has no changes for file #{our_path}"
+        end
+
+        resolution
+      end
+
+      def highlight_lines!
+        their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+        our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+
+        their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
+        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
+        end
+      end
+
+      def sections
+        return @sections if @sections
+
+        chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
+        match_line = nil
+
+        sections_count = chunked_lines.size
+
+        @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
+          section = nil
+
+          # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
+          # always shown in full.
+          if no_conflict
+            conflict_before = i > 0
+            conflict_after = (sections_count - i) > 1
+
+            if conflict_before && conflict_after
+              # Create a gap in a long context section.
+              if lines.length > CONTEXT_LINES * 2
+                head_lines = lines.first(CONTEXT_LINES)
+                tail_lines = lines.last(CONTEXT_LINES)
+
+                # Ensure any existing match line has text for all lines up to the last
+                # line of its context.
+                update_match_line_text(match_line, head_lines.last)
+
+                # Insert a new match line after the created gap.
+                match_line = create_match_line(tail_lines.first)
+
+                section = [
+                  { conflict: false, lines: head_lines },
+                  { conflict: false, lines: tail_lines.unshift(match_line) }
+                ]
+              end
+            elsif conflict_after
+              tail_lines = lines.last(CONTEXT_LINES)
+
+              # Create a gap and insert a match line at the start.
+              if lines.length > tail_lines.length
+                match_line = create_match_line(tail_lines.first)
+
+                tail_lines.unshift(match_line)
+              end
+
+              lines = tail_lines
+            elsif conflict_before
+              # We're at the end of the file (no conflicts after), so just remove extra
+              # trailing lines.
+              lines = lines.first(CONTEXT_LINES)
+            end
+          end
+
+          # We want to update the match line's text every time unless we've already
+          # created a gap and its corresponding match line.
+          update_match_line_text(match_line, lines.last) unless section
+
+          section ||= { conflict: !no_conflict, lines: lines }
+          section[:id] = line_code(lines.first) unless no_conflict
+          section
+        end
+      end
+
+      def line_code(line)
+        Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
+      end
+
+      def create_match_line(line)
+        Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
+      end
+
+      # Any line beginning with a letter, an underscore, or a dollar can be used in a
+      # match line header. Only context sections can contain match lines, as match lines
+      # have to exist in both versions of the file.
+      def find_match_line_header(index)
+        return @match_line_headers[index] if @match_line_headers.key?(index)
+
+        @match_line_headers[index] = begin
+          if index >= 0
+            line = lines[index]
+
+            if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
+              " #{line.text}"
+            else
+              find_match_line_header(index - 1)
+            end
+          end
+        end
+      end
+
+      # Set the match line's text for the current line. A match line takes its start
+      # position and context header (where present) from itself, and its end position from
+      # the line passed in.
+      def update_match_line_text(match_line, line)
+        return unless match_line
+
+        header = find_match_line_header(match_line.index - 1)
+
+        match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
+      end
+
+      def as_json(opts = {})
+        json_hash = {
+          old_path: their_path,
+          new_path: our_path,
+          blob_icon: file_type_icon_class('file', our_mode, our_path),
+          blob_path: namespace_project_blob_path(merge_request.project.namespace,
+                                                 merge_request.project,
+                                                 ::File.join(merge_request.diff_refs.head_sha, our_path))
+        }
+
+        json_hash.tap do |json_hash|
+          if opts[:full_content]
+            json_hash[:content] = content
+            json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
+          else
+            json_hash[:sections] = sections if type.text?
+            json_hash[:type] = type
+            json_hash[:content_path] = content_path
+          end
+        end
+      end
+
+      def content_path
+        conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace,
+                                                               merge_request.project,
+                                                               merge_request,
+                                                               old_path: their_path,
+                                                               new_path: our_path)
+      end
+
+      # Don't try to print merge_request or repository.
+      def inspect
+        instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
+          value = instance_variable_get("@#{instance_variable}")
+
+          "#{instance_variable}=\"#{value}\""
+        end
+
+        "#<#{self.class} #{instance_variables.join(' ')}>"
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa5bd4649d473c619d1a4d75baaf1b703892aaed
--- /dev/null
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -0,0 +1,61 @@
+module Gitlab
+  module Conflict
+    class FileCollection
+      class ConflictSideMissing < StandardError
+      end
+
+      attr_reader :merge_request, :our_commit, :their_commit
+
+      def initialize(merge_request)
+        @merge_request = merge_request
+        @our_commit = merge_request.source_branch_head.raw.raw_commit
+        @their_commit = merge_request.target_branch_head.raw.raw_commit
+      end
+
+      def repository
+        merge_request.project.repository
+      end
+
+      def merge_index
+        @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
+      end
+
+      def files
+        @files ||= merge_index.conflicts.map do |conflict|
+          raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+          Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
+                                     conflict,
+                                     merge_request: merge_request)
+        end
+      end
+
+      def file_for_path(old_path, new_path)
+        files.find { |file| file.their_path == old_path && file.our_path == new_path }
+      end
+
+      def as_json(opts = nil)
+        {
+          target_branch: merge_request.target_branch,
+          source_branch: merge_request.source_branch,
+          commit_sha: merge_request.diff_head_sha,
+          commit_message: default_commit_message,
+          files: files
+        }
+      end
+
+      def default_commit_message
+        conflict_filenames = merge_index.conflicts.map do |conflict|
+          "#   #{conflict[:ours][:path]}"
+        end
+
+        <<EOM.chomp
+Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branch}'
+
+# Conflicts:
+#{conflict_filenames.join("\n")}
+EOM
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddd657903fb6ab08625218bc37ece897ec98143c
--- /dev/null
+++ b/lib/gitlab/conflict/parser.rb
@@ -0,0 +1,76 @@
+module Gitlab
+  module Conflict
+    class Parser
+      class UnresolvableError < StandardError
+      end
+
+      class UnmergeableFile < UnresolvableError
+      end
+
+      class UnsupportedEncoding < UnresolvableError
+      end
+
+      # 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
+
+      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
+
+        line_obj_index = 0
+        line_old = 1
+        line_new = 1
+        type = nil
+        lines = []
+        conflict_start = "<<<<<<< #{our_path}"
+        conflict_middle = '======='
+        conflict_end = ">>>>>>> #{their_path}"
+
+        text.each_line.map do |line|
+          full_line = line.delete("\n")
+
+          if full_line == conflict_start
+            raise UnexpectedDelimiter unless type.nil?
+
+            type = 'new'
+          elsif full_line == conflict_middle
+            raise UnexpectedDelimiter unless type == 'new'
+
+            type = 'old'
+          elsif full_line == conflict_end
+            raise UnexpectedDelimiter unless type == 'old'
+
+            type = nil
+          elsif line[0] == '\\'
+            type = 'nonewline'
+            lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+          else
+            lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+            line_old += 1 if type != 'new'
+            line_new += 1 if type != 'old'
+
+            line_obj_index += 1
+          end
+        end
+
+        raise MissingEndDelimiter unless type.nil?
+
+        lines
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a0f2006bc245b7bc22811d11d85dda466a0fb9a6
--- /dev/null
+++ b/lib/gitlab/conflict/resolution_error.rb
@@ -0,0 +1,6 @@
+module Gitlab
+  module Conflict
+    class ResolutionError < StandardError
+    end
+  end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 9dc2602867e07da8731da1eedb2cb31796a03596..7e3d5647b39dbe826cad2c84cf96557c2c94d8a3 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -1,47 +1,44 @@
 module Gitlab
   class ContributionsCalendar
-    attr_reader :timestamps, :projects, :user
+    attr_reader :contributor
+    attr_reader :current_user
+    attr_reader :projects
 
-    def initialize(projects, user)
-      @projects = projects
-      @user = user
+    def initialize(contributor, current_user = nil)
+      @contributor = contributor
+      @current_user = current_user
+      @projects = ContributedProjectsFinder.new(contributor).execute(current_user)
     end
 
-    def timestamps
-      return @timestamps if @timestamps.present?
+    def activity_dates
+      return @activity_dates if @activity_dates.present?
 
-      @timestamps = {}
+      # Can't use Event.contributions here because we need to check 3 different
+      # project_features for the (currently) 3 different contribution types
       date_from = 1.year.ago
+      repo_events = event_counts(date_from, :repository).
+        having(action: Event::PUSHED)
+      issue_events = event_counts(date_from, :issues).
+        having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
+      mr_events = event_counts(date_from, :merge_requests).
+        having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
 
-      events = Event.reorder(nil).contributions.where(author_id: user.id).
-        where("created_at > ?", date_from).where(project_id: projects).
-        group('date(created_at)').
-        select('date(created_at) as date, count(id) as total_amount').
-        map(&:attributes)
+      union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
+      events = Event.find_by_sql(union.to_sql).map(&:attributes)
 
-      dates = (1.year.ago.to_date..Date.today).to_a
-
-      dates.each do |date|
-        date_id = date.to_time.to_i.to_s
-        @timestamps[date_id] = 0
-        day_events = events.find { |day_events| day_events["date"] == date }
-
-        if day_events
-          @timestamps[date_id] = day_events["total_amount"]
-        end
+      @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
+        activities[event["date"]] += event["total_amount"]
       end
-
-      @timestamps
     end
 
     def events_by_date(date)
-      events = Event.contributions.where(author_id: user.id).
-        where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
+      events = Event.contributions.where(author_id: contributor.id).
+        where(created_at: date.beginning_of_day..date.end_of_day).
         where(project_id: projects)
 
-      events.select do |event|
-        event.push? || event.issue? || event.merge_request?
-      end
+      # Use visible_to_user? instead of the complicated logic in activity_dates
+      # because we're only viewing the events for a single day.
+      events.select {|event| event.visible_to_user?(current_user) }
     end
 
     def starting_year
@@ -51,5 +48,30 @@ module Gitlab
     def starting_month
       Date.today.month
     end
+
+    private
+
+    def event_counts(date_from, feature)
+      t = Event.arel_table
+
+      # re-running the contributed projects query in each union is expensive, so
+      # use IN(project_ids...) instead. It's the intersection of two users so
+      # the list will be (relatively) short
+      @contributed_project_ids ||= projects.uniq.pluck(:id)
+      authed_projects = Project.where(id: @contributed_project_ids).
+        with_feature_available_for_user(feature, current_user).
+        reorder(nil).
+        select(:id)
+
+      conditions = t[:created_at].gteq(date_from.beginning_of_day).
+        and(t[:created_at].lteq(Date.today.end_of_day)).
+        and(t[:author_id].eq(contributor.id))
+
+      Event.reorder(nil).
+        select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount').
+        group(t[:project_id], t[:target_type], t[:action], 'date(created_at)').
+        where(conditions).
+        having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql)))
+    end
   end
 end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 735331df66cf4c72acfb0bc0b4c5a3e23c143a0a..ef9160d64379c9b5a7f9aa80a7b8cf1e62675b88 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -30,6 +30,7 @@ module Gitlab
         signup_enabled: Settings.gitlab['signup_enabled'],
         signin_enabled: Settings.gitlab['signin_enabled'],
         gravatar_enabled: Settings.gravatar['enabled'],
+        koding_enabled: false,
         sign_in_text: nil,
         after_sign_up_text: nil,
         help_page_text: nil,
@@ -40,7 +41,7 @@ module Gitlab
         default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
         default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
         domain_whitelist: Settings.gitlab['domain_whitelist'],
-        import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+        import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
         shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
         max_artifacts_size: Settings.artifacts['max_size'],
         require_two_factor_authentication: false,
@@ -58,10 +59,8 @@ module Gitlab
       # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
       active_db_connection = ActiveRecord::Base.connection.active? rescue false
 
-      ENV['USE_DB'] != 'false' &&
       active_db_connection &&
-      ActiveRecord::Base.connection.table_exists?('application_settings')
-
+        ActiveRecord::Base.connection.table_exists?('application_settings')
     rescue ActiveRecord::NoDatabaseError
       false
     end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 4f81863da35a917b6976991bb14b9aed87887090..d76aa38f74174b9c2c8ff3be95b2f01d2b9a3e6d 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -83,7 +83,7 @@ module Gitlab
           tag = repository.find_tag(tag_name)
 
           if tag
-            commit = repository.commit(tag.target)
+            commit = repository.commit(tag.dereferenced_target)
             commit.try(:sha)
           end
         else
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6a89f715fdaaa725a5546682d165c2beac28128
--- /dev/null
+++ b/lib/gitlab/database/date_time.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module Database
+    module DateTime
+      # Find the first of the `end_time_attrs` that isn't `NULL`. Subtract from it
+      # the first of the `start_time_attrs` that isn't NULL. `SELECT` the resulting interval
+      # along with an alias specified by the `as` parameter.
+      #
+      # Note: For MySQL, the interval is returned in seconds.
+      #       For PostgreSQL, the interval is returned as an INTERVAL type.
+      def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
+        diff_fn = if Gitlab::Database.postgresql?
+                    Arel::Nodes::Subtraction.new(
+                      Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+                      Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+                  elsif Gitlab::Database.mysql?
+                    Arel::Nodes::NamedFunction.new(
+                      "TIMESTAMPDIFF",
+                      [Arel.sql('second'),
+                       Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+                       Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+                  end
+
+        query_so_far.project(diff_fn.as(as))
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1444d25ebc7563540ded9cd95846158ad369a349
--- /dev/null
+++ b/lib/gitlab/database/median.rb
@@ -0,0 +1,112 @@
+# https://www.periscopedata.com/blog/medians-in-sql.html
+module Gitlab
+  module Database
+    module Median
+      def median_datetime(arel_table, query_so_far, column_sym)
+        median_queries =
+          if Gitlab::Database.postgresql?
+            pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+          elsif Gitlab::Database.mysql?
+            mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+          end
+
+        results = Array.wrap(median_queries).map do |query|
+          ActiveRecord::Base.connection.execute(query)
+        end
+        extract_median(results).presence
+      end
+
+      def extract_median(results)
+        result = results.compact.first
+
+        if Gitlab::Database.postgresql?
+          result = result.first.presence
+          median = result['median'] if result
+          median.to_f if median
+        elsif Gitlab::Database.mysql?
+          result.to_a.flatten.first
+        end
+      end
+
+      def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+        query = arel_table.
+                from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)).
+                project(average([arel_table[column_sym]], 'median')).
+                where(
+                  Arel::Nodes::Between.new(
+                    Arel.sql("(select @row_id := @row_id + 1)"),
+                    Arel::Nodes::And.new(
+                      [Arel.sql('@ct/2.0'),
+                       Arel.sql('@ct/2.0 + 1')]
+                    )
+                  )
+                ).
+                # Disallow negative values
+                where(arel_table[column_sym].gteq(0))
+
+        [
+          Arel.sql("CREATE TEMPORARY TABLE IF NOT EXISTS #{query_so_far.to_sql}"),
+          Arel.sql("set @ct := (select count(1) from #{arel_table.table_name});"),
+          Arel.sql("set @row_id := 0;"),
+          query.to_sql,
+          Arel.sql("DROP TEMPORARY TABLE IF EXISTS #{arel_table.table_name};")
+        ]
+      end
+
+      def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+        # Create a CTE with the column we're operating on, row number (after sorting by the column
+        # we're operating on), and count of the table we're operating on (duplicated across) all rows
+        # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
+        # column, the CTE might look like this:
+        #
+        #  star_count | row_id | ct
+        # ------------+--------+----
+        #           5 |      1 |  3
+        #           9 |      2 |  3
+        #          15 |      3 |  3
+        cte_table = Arel::Table.new("ordered_records")
+        cte = Arel::Nodes::As.new(
+          cte_table,
+          arel_table.
+            project(
+              arel_table[column_sym].as(column_sym.to_s),
+              Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
+                                    Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
+              arel_table.project("COUNT(1)").as('ct')).
+            # Disallow negative values
+            where(arel_table[column_sym].gteq(zero_interval)))
+
+        # From the CTE, select either the middle row or the middle two rows (this is accomplished
+        # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
+        # selected rows, and this is the median value.
+        cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")).
+          where(
+            Arel::Nodes::Between.new(
+              cte_table[:row_id],
+              Arel::Nodes::And.new(
+                [(cte_table[:ct] / Arel.sql('2.0')),
+                 (cte_table[:ct] / Arel.sql('2.0') + 1)]
+              )
+            )
+          ).
+          with(query_so_far, cte).
+          to_sql
+      end
+
+      private
+
+      def average(args, as)
+        Arel::Nodes::NamedFunction.new("AVG", args, as)
+      end
+
+      def extract_epoch(arel_attribute)
+        Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
+      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")])
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 927f9dad20bee1c16f1d0104c1a395f7dc4a3b5a..0bd6e148ba8d5419bde0040c2b345d836f5ca11b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -129,12 +129,14 @@ module Gitlab
       # column - The name of the column to add.
       # type - The column type (e.g. `:integer`).
       # default - The default value for the column.
+      # limit - Sets a column limit. For example, for :integer, the default is
+      #         4-bytes. Set `limit: 8` to allow 8-byte integers.
       # allow_null - When set to `true` the column will allow NULL values, the
       #              default is to not allow NULL values.
       #
       # This method can also take a block which is passed directly to the
       # `update_column_in_batches` method.
-      def add_column_with_default(table, column, type, default:, allow_null: false, &block)
+      def add_column_with_default(table, column, type, default:, limit: nil, allow_null: false, &block)
         if transaction_open?
           raise 'add_column_with_default can not be run inside a transaction, ' \
             'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -144,7 +146,11 @@ module Gitlab
         disable_statement_timeout
 
         transaction do
-          add_column(table, column, type, default: nil)
+          if limit
+            add_column(table, column, type, default: nil, limit: limit)
+          else
+            add_column(table, column, type, default: nil)
+          end
 
           # Changing the default before the update ensures any newly inserted
           # rows already use the proper default value.
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index e47df508ca29fa570bbc699893c0d4219591b775..ce85e5e0123fc7ce14d2dc34a9673d3da532b496 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -125,6 +125,10 @@ module Gitlab
 
         repository.blob_at(commit.id, file_path)
       end
+
+      def cache_key
+        "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
+      end
     end
   end
 end
diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
similarity index 70%
rename from lib/gitlab/diff/file_collection/merge_request.rb
rename to lib/gitlab/diff/file_collection/merge_request_diff.rb
index 4f946908e2f9c63eb42ad71030d0511ad71402f3..dc4d47c878b22880bc967f34132c32fdfa3e50fd 100644
--- a/lib/gitlab/diff/file_collection/merge_request.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -1,14 +1,14 @@
 module Gitlab
   module Diff
     module FileCollection
-      class MergeRequest < Base
-        def initialize(merge_request, diff_options:)
-          @merge_request = merge_request
+      class MergeRequestDiff < Base
+        def initialize(merge_request_diff, diff_options:)
+          @merge_request_diff = merge_request_diff
 
-          super(merge_request,
-            project: merge_request.project,
+          super(merge_request_diff,
+            project: merge_request_diff.project,
             diff_options: diff_options,
-            diff_refs: merge_request.diff_refs)
+            diff_refs: merge_request_diff.diff_refs)
         end
 
         def diff_files
@@ -35,16 +35,16 @@ module Gitlab
         # for the highlighted ones, so we just skip their execution.
         # If the highlighted diff files lines are not cached we calculate and cache them.
         #
-        # The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of
+        # The content of the cache is a Hash where the key identifies the file and the values are Arrays of
         # hashes that represent serialized diff lines.
         #
         def cache_highlight!(diff_file)
-          file_path = diff_file.file_path
+          item_key = diff_file.cache_key
 
-          if highlight_cache[file_path]
-            highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path])
+          if highlight_cache[item_key]
+            highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
           else
-            highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
+            highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash)
           end
         end
 
@@ -61,11 +61,11 @@ module Gitlab
         end
 
         def cacheable?
-          @merge_request.merge_request_diff.present?
+          @merge_request_diff.present?
         end
 
         def cache_key
-          [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options]
+          [@merge_request_diff, 'highlighted-diff-files', diff_options]
         end
       end
     end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index cf097e0d0dec32f6f782efeca98be06e30328638..80a146b4a5a96f490b0252b5404232486ad8a4aa 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -2,11 +2,13 @@ module Gitlab
   module Diff
     class Line
       attr_reader :type, :index, :old_pos, :new_pos
+      attr_writer :rich_text
       attr_accessor :text
 
-      def initialize(text, type, index, old_pos, new_pos)
+      def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
         @text, @type, @index = text, type, index
         @old_pos, @new_pos = old_pos, new_pos
+        @parent_file = parent_file
       end
 
       def self.init_from_hash(hash)
@@ -43,9 +45,25 @@ module Gitlab
         type == 'old'
       end
 
+      def rich_text
+        @parent_file.highlight_lines! if @parent_file && !@rich_text
+
+        @rich_text
+      end
+
       def meta?
         type == 'match' || type == 'nonewline'
       end
+
+      def as_json(opts = nil)
+        {
+          type: type,
+          old_line: old_line,
+          new_line: new_line,
+          text: text,
+          rich_text: rich_text || text
+        }
+      end
     end
   end
 end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 2fdcf8d7838d7e572066228abaa5cdd2d976d70a..ecf62dead350fd54c4f45079829a75201e62ee5f 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -139,13 +139,19 @@ module Gitlab
       private
 
       def find_diff_file(repository)
-        diffs = Gitlab::Git::Compare.new(
-          repository.raw_repository,
-          start_sha,
-          head_sha
-        ).diffs(paths: paths)
+        # 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
+
+        diff = compare.diffs(paths: paths).first
 
-        diff = diffs.first
         return unless diff
 
         Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 4446e921e0df989d2cd0b7a54dafd84388becfe9..40a4815a9a02ba320d8ab71c4e075ffe96be9f20 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -1,10 +1,10 @@
 module Gitlab
   class DowntimeCheck
     class Message
-      attr_reader :path, :offline, :reason
+      attr_reader :path, :offline
 
-      OFFLINE = "\e[32moffline\e[0m"
-      ONLINE = "\e[31monline\e[0m"
+      OFFLINE = "\e[31moffline\e[0m"
+      ONLINE = "\e[32monline\e[0m"
 
       # path - The file path of the migration.
       # offline - When set to `true` the migration will require downtime.
@@ -19,10 +19,21 @@ module Gitlab
         label = offline ? OFFLINE : ONLINE
 
         message = "[#{label}]: #{path}"
-        message += ": #{reason}" if reason
+
+        if reason?
+          message += ":\n\n#{reason}\n\n"
+        end
 
         message
       end
+
+      def reason?
+        @reason.present?
+      end
+
+      def reason
+        @reason.strip.lines.map(&:strip).join("\n")
+      end
     end
   end
 end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4d1505ea918ac5fe25629c714981558818155e4
--- /dev/null
+++ b/lib/gitlab/ee_compat_check.rb
@@ -0,0 +1,271 @@
+# rubocop: disable Rails/Output
+module Gitlab
+  # Checks if a set of migrations requires downtime or not.
+  class EeCompatCheck
+    CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
+    EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+    CHECK_DIR = Rails.root.join('ee_compat_check')
+    MAX_FETCH_DEPTH = 500
+    IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
+
+    attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
+
+    def initialize(branch:, ce_repo: CE_REPO)
+      @repo_dir = CHECK_DIR.join('repo')
+      @patches_dir = CHECK_DIR.join('patches')
+      @ce_branch = branch
+      @ce_repo = ce_repo
+    end
+
+    def check
+      ensure_ee_repo
+      ensure_patches_dir
+
+      generate_patch(ce_branch, ce_patch_full_path)
+
+      Dir.chdir(repo_dir) do
+        step("In the #{repo_dir} directory")
+
+        status = catch(:halt_check) do
+          ce_branch_compat_check!
+          delete_ee_branch_locally!
+          ee_branch_presence_check!
+          ee_branch_compat_check!
+        end
+
+        delete_ee_branch_locally!
+
+        if status.nil?
+          true
+        else
+          false
+        end
+      end
+    end
+
+    private
+
+    def ensure_ee_repo
+      if Dir.exist?(repo_dir)
+        step("#{repo_dir} already exists")
+      else
+        cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
+        step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
+      end
+    end
+
+    def ensure_patches_dir
+      FileUtils.mkdir_p(patches_dir)
+    end
+
+    def generate_patch(branch, patch_path)
+      FileUtils.rm(patch_path, force: true)
+
+      depth = 0
+      loop do
+        depth += 50
+        cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
+        Gitlab::Popen.popen(cmd)
+        _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
+
+        raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
+        break if status.zero?
+      end
+
+      step("Generating the patch against master in #{patch_path}")
+      output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
+      throw(:halt_check, :ko) unless status.zero?
+
+      File.write(patch_path, output)
+      throw(:halt_check, :ko) unless File.exist?(patch_path)
+    end
+
+    def ce_branch_compat_check!
+      if check_patch(ce_patch_full_path).zero?
+        puts applies_cleanly_msg(ce_branch)
+        throw(:halt_check)
+      end
+    end
+
+    def ee_branch_presence_check!
+      status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}])
+
+      unless status.zero?
+        puts
+        puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+
+        throw(:halt_check, :ko)
+      end
+    end
+
+    def ee_branch_compat_check!
+      step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
+
+      generate_patch(ee_branch, ee_patch_full_path)
+
+      unless check_patch(ee_patch_full_path).zero?
+        puts
+        puts ee_branch_doesnt_apply_cleanly_msg
+
+        throw(:halt_check, :ko)
+      end
+
+      puts
+      puts applies_cleanly_msg(ee_branch)
+    end
+
+    def check_patch(patch_path)
+      step("Checking out master", %w[git checkout master])
+      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}])
+
+      unless status.zero?
+        failed_files = output.lines.reduce([]) do |memo, line|
+          if line.start_with?('error: patch failed:')
+            file = line.sub(/\Aerror: patch failed: /, '')
+            memo << file unless file =~ IGNORED_FILES_REGEX
+          end
+          memo
+        end
+
+        if failed_files.empty?
+          status = 0
+        else
+          puts "\nConflicting files:"
+          failed_files.each do |file|
+            puts "  - #{file}"
+          end
+        end
+      end
+
+      status
+    end
+
+    def delete_ee_branch_locally!
+      command(%w[git checkout master])
+      step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
+    end
+
+    def ce_patch_name
+      @ce_patch_name ||= "#{ce_branch}.patch"
+    end
+
+    def ce_patch_full_path
+      @ce_patch_full_path ||= patches_dir.join(ce_patch_name)
+    end
+
+    def ee_branch
+      @ee_branch ||= "#{ce_branch}-ee"
+    end
+
+    def ee_patch_name
+      @ee_patch_name ||= "#{ee_branch}.patch"
+    end
+
+    def ee_patch_full_path
+      @ee_patch_full_path ||= patches_dir.join(ee_patch_name)
+    end
+
+    def step(desc, cmd = nil)
+      puts "\n=> #{desc}\n"
+
+      if cmd
+        start = Time.now
+        puts "\n$ #{cmd.join(' ')}"
+        status = command(cmd)
+        puts "\nFinished in #{Time.now - start} seconds"
+        status
+      end
+    end
+
+    def command(cmd)
+      output, status = Gitlab::Popen.popen(cmd)
+      puts output
+
+      status
+    end
+
+    def applies_cleanly_msg(branch)
+      <<-MSG.strip_heredoc
+        =================================================================
+        🎉 Congratulations!! 🎉
+
+        The #{branch} branch applies cleanly to EE/master!
+
+        Much ❤️!!
+        =================================================================\n
+      MSG
+    end
+
+    def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+      <<-MSG.strip_heredoc
+        =================================================================
+        💥 Oh no! 💥
+
+        The #{ce_branch} branch does not apply cleanly to the current
+        EE/master, and no #{ee_branch} branch was found in the EE repository.
+
+        Please create a #{ee_branch} branch that includes changes from
+        #{ce_branch} but also specific changes than can be applied cleanly
+        to EE/master.
+
+        There are different ways to create such branch:
+
+        1. Create a new branch based on the CE branch and rebase it on top of EE/master
+
+          # In the EE repo
+          $ git fetch #{ce_repo} #{ce_branch}
+          $ git checkout -b #{ee_branch} FETCH_HEAD
+
+          # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit
+          # before rebasing to limit the conflicts-resolving steps during the rebase
+          $ git fetch origin
+          $ git rebase origin/master
+
+          At this point you will likely have conflicts.
+          Solve them, and continue/finish the rebase.
+
+          You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE".
+
+        2. Create a new branch from master and cherry-pick your CE commits
+
+          # In the EE repo
+          $ git fetch origin
+          $ git checkout -b #{ee_branch} origin/master
+          $ git fetch #{ce_repo} #{ce_branch}
+          $ git cherry-pick SHA # Repeat for all the commits you want to pick
+
+          You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit.
+
+        Don't forget to push your branch to #{EE_REPO}:
+
+          # In the EE repo
+          $ git push origin #{ee_branch}
+
+        You can then retry this failed build, and hopefully it should pass.
+
+        Stay 💪 !
+        =================================================================\n
+      MSG
+    end
+
+    def ee_branch_doesnt_apply_cleanly_msg
+      <<-MSG.strip_heredoc
+        =================================================================
+        💥 Oh no! 💥
+
+        The #{ce_branch} does not apply cleanly to the current
+        EE/master, and even though a #{ee_branch} branch exists in the EE
+        repository, it does not apply cleanly either to EE/master!
+
+        Please update the #{ee_branch}, push it again to #{EE_REPO}, and
+        retry this build.
+
+        Stay 💪 !
+        =================================================================\n
+      MSG
+    end
+  end
+end
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index b7ed11cb6389e06cdbeccd81a4b0134534d5636e..7cccf465334f5ceacfc68b2809ca704e73ac7aab 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -45,6 +45,7 @@ module Gitlab
 
         def verify_record!(record:, invalid_exception:, record_name:)
           return if record.persisted?
+          return if record.errors.key?(:commands_only)
 
           error_title = "The #{record_name} could not be created for the following reasons:"
 
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 4e6566af8abed30fb7658266c8ec2897802fa23b..9f90a3ec2b2f70645ae27db6dbabf5557d31cfb8 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -5,16 +5,16 @@ module Gitlab
   module Email
     module Handler
       class CreateIssueHandler < BaseHandler
-        attr_reader :project_path, :authentication_token
+        attr_reader :project_path, :incoming_email_token
 
         def initialize(mail, mail_key)
           super(mail, mail_key)
-          @project_path, @authentication_token =
+          @project_path, @incoming_email_token =
             mail_key && mail_key.split('+', 2)
         end
 
         def can_handle?
-          !authentication_token.nil?
+          !incoming_email_token.nil?
         end
 
         def execute
@@ -29,7 +29,7 @@ module Gitlab
         end
 
         def author
-          @author ||= User.find_by(authentication_token: authentication_token)
+          @author ||= User.find_by(incoming_email_token: incoming_email_token)
         end
 
         def project
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 06dae31cc27e7ce139c6a7476918df83c34141a8..447c7a6a6b9465e63ea3c8ada822cef1d13b0233 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -46,7 +46,9 @@ module Gitlab
             noteable_type:  sent_notification.noteable_type,
             noteable_id:    sent_notification.noteable_id,
             commit_id:      sent_notification.commit_id,
-            line_code:      sent_notification.line_code
+            line_code:      sent_notification.line_code,
+            position:       sent_notification.position,
+            type:           sent_notification.note_type
           ).execute
         end
       end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index b63213ae208cc3616dc535bbcd663b824464d1b9..bbbca8acc40316a618c4ca6ae86f839e63bb8be6 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -10,12 +10,20 @@ module Gitlab
       Gemojione.index.instance_variable_get(:@emoji_by_moji)
     end
 
+    def emojis_unicodes
+      emojis_by_moji.keys
+    end
+
     def emojis_names
-      emojis.keys.sort
+      emojis.keys
     end
 
     def emoji_filename(name)
       emojis[name]["unicode"]
     end
+
+    def emoji_unicode_filename(moji)
+      emojis_by_moji[moji]["unicode"]
+    end
   end
 end
diff --git a/lib/gitlab/production_logger.rb b/lib/gitlab/environment_logger.rb
similarity index 50%
rename from lib/gitlab/production_logger.rb
rename to lib/gitlab/environment_logger.rb
index 89ce7144b1bce57881ac850ee8cbfbfdcc250adb..407cc572656614ea323eac43dee31b95a95a1a26 100644
--- a/lib/gitlab/production_logger.rb
+++ b/lib/gitlab/environment_logger.rb
@@ -1,7 +1,7 @@
 module Gitlab
-  class ProductionLogger < Gitlab::Logger
+  class EnvironmentLogger < Gitlab::Logger
     def self.file_name_noext
-      'production'
+      Rails.env
     end
   end
 end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index ffe49364379b253df0c45d9bb061d1765401bdc0..2dd427043962581265f199dd0dc389960827a847 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -1,59 +1,52 @@
+require 'securerandom'
+
 module Gitlab
   # This class implements an 'exclusive lease'. We call it a 'lease'
   # because it has a set expiry time. We call it 'exclusive' because only
   # one caller may obtain a lease for a given key at a time. The
   # implementation is intended to work across GitLab processes and across
-  # servers. It is a 'cheap' alternative to using SQL queries and updates:
+  # servers. It is a cheap alternative to using SQL queries and updates:
   # you do not need to change the SQL schema to start using
   # ExclusiveLease.
   #
-  # It is important to choose the timeout wisely. If the timeout is very
-  # high (1 hour) then the throughput of your operation gets very low (at
-  # most once an hour). If the timeout is lower than how long your
-  # operation may take then you cannot count on exclusivity. For example,
-  # if the timeout is 10 seconds and you do an operation which may take 20
-  # seconds then two overlapping operations may hold a lease for the same
-  # key at the same time.
-  #
-  # This class has no 'cancel' method. I originally decided against adding
-  # it because it would add complexity and a false sense of security. The
-  # complexity: instead of setting '1' we would have to set a UUID, and to
-  # delete it we would have to execute Lua on the Redis server to only
-  # delete the key if the value was our own UUID. Otherwise there is a
-  # chance that when you intend to cancel your lease you actually delete
-  # someone else's. The false sense of security: you cannot design your
-  # system to rely too much on the lease being cancelled after use because
-  # the calling (Ruby) process may crash or be killed. You _cannot_ count
-  # on begin/ensure blocks to cancel a lease, because the 'ensure' does
-  # not always run. Think of 'kill -9' from the Unicorn master for
-  # instance.
-  # 
-  # If you find that leases are getting in your way, ask yourself: would
-  # it be enough to lower the lease timeout? Another thing that might be
-  # appropriate is to only use a lease for bulk/automated operations, and
-  # to ignore the lease when you get a single 'manual' user request (a
-  # button click).
-  #
   class ExclusiveLease
+    LUA_CANCEL_SCRIPT = <<-EOS
+      local key, uuid = KEYS[1], ARGV[1]
+      if redis.call("get", key) == uuid then
+        redis.call("del", key)
+      end
+    EOS
+
+    def self.cancel(key, uuid)
+      Gitlab::Redis.with do |redis|
+        redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_key(key)], argv: [uuid])
+      end
+    end
+
+    def self.redis_key(key)
+      "gitlab:exclusive_lease:#{key}"
+    end
+
     def initialize(key, timeout:)
-      @key, @timeout = key, timeout
+      @redis_key = self.class.redis_key(key)
+      @timeout = timeout
+      @uuid = SecureRandom.uuid
     end
 
-    # Try to obtain the lease. Return true on success,
+    # Try to obtain the lease. Return lease UUID on success,
     # false if the lease is already taken.
     def try_obtain
       # Performing a single SET is atomic
       Gitlab::Redis.with do |redis|
-        !!redis.set(redis_key, '1', nx: true, ex: @timeout)
+        redis.set(@redis_key, @uuid, nx: true, ex: @timeout) && @uuid
       end
     end
 
-    # No #cancel method. See comments above!
-
-    private
-
-    def redis_key
-      "gitlab:exclusive_lease:#{@key}"
+    # Returns true if the key for this lease is set.
+    def exists?
+      Gitlab::Redis.with do |redis|
+        redis.exists(@redis_key)
+      end
     end
   end
 end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 501d5a955478abb6aa4cc112e1446bfe1d313d30..222bcdcbf9c8a569bc666c20d02db599e17760ea 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -74,8 +74,8 @@ module Gitlab
       end
 
       def create_label(name)
-        color = nice_label_color(name)
-        Label.create!(project_id: project.id, title: name, color: color)
+        params = { title: name, color: nice_label_color(name) }
+        ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
       end
 
       def user_info(person_id)
@@ -122,25 +122,21 @@ module Gitlab
           author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
 
           issue = Issue.create!(
-            project_id:   project.id,
-            title:        bug['sTitle'],
-            description:  body,
-            author_id:    author_id,
-            assignee_id:  assignee_id,
-            state:        bug['fOpen'] == 'true' ? 'opened' : 'closed'
+            iid:         bug['ixBug'],
+            project_id:  project.id,
+            title:       bug['sTitle'],
+            description: body,
+            author_id:   author_id,
+            assignee_id: assignee_id,
+            state:       bug['fOpen'] == 'true' ? 'opened' : 'closed',
+            created_at:  date,
+            updated_at:  DateTime.parse(bug['dtLastUpdated'])
           )
-          issue.add_labels_by_names(labels)
 
-          if issue.iid != bug['ixBug']
-            issue.update_attribute(:iid, bug['ixBug'])
-          end
+          issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
+          issue.update_attribute(:label_ids, issue_labels.pluck(:id))
 
           import_issue_comments(issue, comments)
-
-          issue.update_attribute(:created_at, date)
-
-          last_update = DateTime.parse(bug['dtLastUpdated'])
-          issue.update_attribute(:updated_at, last_update)
         end
       end
 
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 78d7a4f27cf4c39fedbc839bc4079d6dd92a3f2e..a7c596dced0198f00de097ffe3b3ea5f54cc6860 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -58,7 +58,7 @@ module Gitlab
         referable = find_referable(reference)
         return reference unless referable
 
-        cross_reference = referable.to_reference(target_project)
+        cross_reference = build_cross_reference(referable, target_project)
         return reference if reference == cross_reference
 
         new_text = before + cross_reference + after
@@ -72,6 +72,14 @@ module Gitlab
         extractor.all.first
       end
 
+      def build_cross_reference(referable, target_project)
+        if referable.respond_to?(:project)
+          referable.to_reference(target_project)
+        else
+          referable.to_reference(@source_project, target_project)
+        end
+      end
+
       def substitution_valid?(substituted)
         @original_html == markdown(substituted)
       end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 7584efe4fa864dde1a23c253dde68037841dff58..3cd515e4a3ab742fedafa36320aadee1167fa1fc 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -18,6 +18,16 @@ module Gitlab
         end
       end
 
+      def committer_hash(email:, name:)
+        return if email.nil? || name.nil?
+
+        {
+          email: email,
+          name: name,
+          time: Time.now
+        }
+      end
+
       def tag_name(ref)
         ref = ref.to_s
         if self.tag_ref?(ref)
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 9b681e636c7fcc68171e4624a4b2f49d7d64639c..bd90d24a2ecd333477cf96e530215a935f562f39 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -17,11 +17,13 @@ module Gitlab
       def trigger(gl_id, oldrev, newrev, ref)
         return [true, nil] unless exists?
 
-        case name
-        when "pre-receive", "post-receive"
-          call_receive_hook(gl_id, oldrev, newrev, ref)
-        when "update"
-          call_update_hook(gl_id, oldrev, newrev, ref)
+        Bundler.with_clean_env do
+          case name
+          when "pre-receive", "post-receive"
+            call_receive_hook(gl_id, oldrev, newrev, ref)
+          when "update"
+            call_update_hook(gl_id, oldrev, newrev, ref)
+          end
         end
       end
 
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index f7432df55579d18d412371e3425e6f89c984cef5..64b5c4b98dc6fca85b83028364668a4d15ce0305 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,50 +2,52 @@
 # class return an instance of `GitlabAccessStatus`
 module Gitlab
   class GitAccess
+    UnauthorizedError = Class.new(StandardError)
+
+    ERROR_MESSAGES = {
+      upload: 'You are not allowed to upload code for this project.',
+      download: 'You are not allowed to download code from this project.',
+      deploy_key: 'Deploy keys are not allowed to push code.',
+      no_repo: 'A repository for this project does not exist yet.'
+    }
+
     DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
     PUSH_COMMANDS = %w{ git-receive-pack }
+    ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
 
-    attr_reader :actor, :project, :protocol, :user_access
+    attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
 
-    def initialize(actor, project, protocol)
+    def initialize(actor, project, protocol, authentication_abilities:)
       @actor    = actor
       @project  = project
       @protocol = protocol
+      @authentication_abilities = authentication_abilities
       @user_access = UserAccess.new(user, project: project)
     end
 
     def check(cmd, changes)
-      return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
-
-      unless actor
-        return build_status_object(false, "No user or key was provided.")
-      end
-
-      if user && !user_access.allowed?
-        return build_status_object(false, "Your account has been blocked.")
-      end
-
-      unless project && (user_access.can_read_project? || deploy_key_can_read_project?)
-        return build_status_object(false, 'The project you were looking for could not be found.')
-      end
+      check_protocol!
+      check_active_user!
+      check_project_accessibility!
+      check_command_existence!(cmd)
 
       case cmd
       when *DOWNLOAD_COMMANDS
         download_access_check
       when *PUSH_COMMANDS
         push_access_check(changes)
-      else
-        build_status_object(false, "The command you're trying to execute is not allowed.")
       end
+
+      build_status_object(true)
+    rescue UnauthorizedError => ex
+      build_status_object(false, ex.message)
     end
 
     def download_access_check
       if user
         user_download_access_check
-      elsif deploy_key
-        build_status_object(true)
-      else
-        raise 'Wrong actor'
+      elsif deploy_key.nil? && !Guest.can?(:download_code, project)
+        raise UnauthorizedError, ERROR_MESSAGES[:download]
       end
     end
 
@@ -59,21 +61,35 @@ module Gitlab
       elsif deploy_key
         deploy_key_push_access_check(changes)
       else
-        raise 'Wrong actor'
+        raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
       end
     end
 
     def user_download_access_check
-      unless user_access.can_do_action?(:download_code)
-        return build_status_object(false, "You are not allowed to download code from this project.")
+      unless user_can_download_code? || build_can_download_code?
+        raise UnauthorizedError, ERROR_MESSAGES[:download]
       end
+    end
 
-      build_status_object(true)
+    def user_can_download_code?
+      authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
+    end
+
+    def build_can_download_code?
+      authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
     end
 
     def user_push_access_check(changes)
+      unless authentication_abilities.include?(:push_code)
+        raise UnauthorizedError, ERROR_MESSAGES[:upload]
+      end
+
       if changes.blank?
-        return build_status_object(true)
+        return # Allow access.
+      end
+
+      unless project.repository.exists?
+        raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
       end
 
       changes_list = Gitlab::ChangesList.new(changes)
@@ -83,11 +99,9 @@ module Gitlab
         status = change_access_check(change)
         unless status.allowed?
           # If user does not have access to make at least one change - cancel all push
-          return status
+          raise UnauthorizedError, status.message
         end
       end
-
-      build_status_object(true)
     end
 
     def deploy_key_push_access_check(changes)
@@ -108,6 +122,30 @@ module Gitlab
 
     private
 
+    def check_protocol!
+      unless protocol_allowed?
+        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
+      end
+    end
+
+    def check_active_user!
+      if user && !user_access.allowed?
+        raise UnauthorizedError, "Your account has been blocked."
+      end
+    end
+
+    def check_project_accessibility!
+      if project.blank? || !can_read_project?
+        raise UnauthorizedError, 'The project you were looking for could not be found.'
+      end
+    end
+
+    def check_command_existence!(cmd)
+      unless ALL_COMMANDS.include?(cmd)
+        raise UnauthorizedError, "The command you're trying to execute is not allowed."
+      end
+    end
+
     def matching_merge_request?(newrev, branch_name)
       Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
     end
@@ -125,6 +163,16 @@ module Gitlab
       end
     end
 
+    def can_read_project?
+      if user
+        user_access.can_read_project?
+      elsif deploy_key
+        deploy_key_can_read_project?
+      else
+        Guest.can?(:read_project, project)
+      end
+    end
+
     protected
 
     def user
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 72992baffd40df8abbbce6dc8512269d2e264772..6dbae64a9fe767b21a977cb48f2abc5c7d429d8a 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -10,16 +10,23 @@ module Gitlab
       end
 
       def create!
-        self.klass.create!(self.attributes)
+        project.public_send(project_association).find_or_create_by!(find_condition) do |record|
+          record.attributes = attributes
+        end
       end
 
       private
 
-      def gl_user_id(github_id)
+      def gitlab_user_id(github_id)
         User.joins(:identities).
           find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
           try(:id)
       end
+
+      def gitlab_author_id
+        return @gitlab_author_id if defined?(@gitlab_author_id)
+        @gitlab_author_id = gitlab_user_id(raw_data.user.id)
+      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 084e514492c2d4337b5154811cca10e36af3bbaf..85df6547a673d61b77fc5eb7af0b7f86c0f21c98 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -52,7 +52,7 @@ module Gitlab
 
       def method_missing(method, *args, &block)
         if api.respond_to?(method)
-          request { api.send(method, *args, &block) }
+          request(method, *args, &block)
         else
           super(method, *args, &block)
         end
@@ -99,20 +99,31 @@ module Gitlab
         rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
       end
 
-      def request
+      def request(method, *args, &block)
         sleep rate_limit_sleep_time if rate_limit_exceed?
 
-        data = yield
+        data = api.send(method, *args)
+        return data unless data.is_a?(Array)
 
         last_response = api.last_response
 
+        if block_given?
+          yield data
+          # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
+          # so we cache our own last response
+          each_response_page(last_response, &block)
+        else
+          each_response_page(last_response) { |page| data.concat(page) }
+          data
+        end
+      end
+
+      def each_response_page(last_response)
         while last_response.rels[:next]
           sleep rate_limit_sleep_time if rate_limit_exceed?
           last_response = last_response.rels[:next].get
-          data.concat(last_response.data) if last_response.data.is_a?(Array)
+          yield last_response.data if last_response.data.is_a?(Array)
         end
-
-        data
       end
     end
   end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2c1b94ef2cd781c1288d48917169994c1a82dfd6..2bddcde2b7cc3cf706f9a0ab7b5385eada19ae19 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -21,7 +21,7 @@ module Gitlab
       end
 
       def author_id
-        gl_user_id(raw_data.user.id) || project.creator_id
+        gitlab_author_id || project.creator_id
       end
 
       def body
@@ -52,7 +52,11 @@ module Gitlab
       end
 
       def note
-        formatter.author_line(author) + body
+        if gitlab_author_id
+          body
+        else
+          formatter.author_line(author) + body
+        end
       end
 
       def type
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 9ddc8905bd6149f32bf58fbb896afc45787613c5..90cf38a8513e9bf83689689037700be99afc115e 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,12 +3,14 @@ module Gitlab
     class Importer
       include Gitlab::ShellAdapter
 
-      attr_reader :client, :project, :repo, :repo_url
+      attr_reader :client, :errors, :project, :repo, :repo_url
 
       def initialize(project)
         @project  = project
         @repo     = project.import_source
         @repo_url = project.import_url
+        @errors   = []
+        @labels   = {}
 
         if credentials
           @client = Client.new(credentials[:user])
@@ -18,8 +20,17 @@ module Gitlab
       end
 
       def execute
-        import_labels && import_milestones && import_issues &&
-          import_pull_requests && import_wiki
+        import_labels
+        import_milestones
+        import_issues
+        import_pull_requests
+        import_comments(:issues)
+        import_comments(:pull_requests)
+        import_wiki
+        import_releases
+        handle_errors
+
+        true
       end
 
       private
@@ -28,63 +39,79 @@ module Gitlab
         @credentials ||= project.import_data.credentials if project.import_data
       end
 
+      def handle_errors
+        return unless errors.any?
+
+        project.update_column(:import_error, {
+          message: 'The remote data could not be fully imported.',
+          errors: errors
+        }.to_json)
+      end
+
       def import_labels
-        labels = client.labels(repo, per_page: 100)
-        labels.each { |raw| LabelFormatter.new(project, raw).create! }
+        fetch_resources(:labels, repo, per_page: 100) do |labels|
+          labels.each do |raw|
+            begin
+              LabelFormatter.new(project, raw).create!
+            rescue => e
+              errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+            end
+          end
+        end
 
-        true
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error, e.message
+        cache_labels!
       end
 
       def import_milestones
-        milestones = client.milestones(repo, state: :all, per_page: 100)
-        milestones.each { |raw| MilestoneFormatter.new(project, raw).create! }
-
-        true
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error, e.message
+        fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
+          milestones.each do |raw|
+            begin
+              MilestoneFormatter.new(project, raw).create!
+            rescue => e
+              errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+            end
+          end
+        end
       end
 
       def import_issues
-        issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
+        fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+          issues.each do |raw|
+            gh_issue = IssueFormatter.new(project, raw)
 
-        issues.each do |raw|
-          gh_issue = IssueFormatter.new(project, raw)
-
-          if gh_issue.valid?
-            issue = gh_issue.create!
-            apply_labels(issue)
-            import_comments(issue) if gh_issue.has_comments?
+            if gh_issue.valid?
+              begin
+                issue = gh_issue.create!
+                apply_labels(issue, raw)
+              rescue => e
+                errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+              end
+            end
           end
         end
-
-        true
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error, e.message
       end
 
       def import_pull_requests
-        pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
-        pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
-
-        pull_requests.each do |pull_request|
-          begin
-            restore_source_branch(pull_request) unless pull_request.source_branch_exists?
-            restore_target_branch(pull_request) unless pull_request.target_branch_exists?
-
-            merge_request = pull_request.create!
-            apply_labels(merge_request)
-            import_comments(merge_request)
-            import_comments_on_diff(merge_request)
-          rescue ActiveRecord::RecordInvalid => e
-            raise Projects::ImportService::Error, e.message
-          ensure
-            clean_up_restored_branches(pull_request)
+        fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+          pull_requests.each do |raw|
+            pull_request = PullRequestFormatter.new(project, raw)
+            next unless pull_request.valid?
+
+            begin
+              restore_source_branch(pull_request) unless pull_request.source_branch_exists?
+              restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+
+              merge_request = pull_request.create!
+              apply_labels(merge_request, raw)
+            rescue => e
+              errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+            ensure
+              clean_up_restored_branches(pull_request)
+            end
           end
         end
 
-        true
+        project.repository.after_remove_branch
       end
 
       def restore_source_branch(pull_request)
@@ -98,63 +125,162 @@ module Gitlab
       def remove_branch(name)
         project.repository.delete_branch(name)
       rescue Rugged::ReferenceError
-        nil
+        errors << { type: :remove_branch, name: name }
       end
 
       def clean_up_restored_branches(pull_request)
         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?
-
-        project.repository.after_remove_branch
       end
 
-      def apply_labels(issuable)
-        issue = client.issue(repo, issuable.iid)
+      def apply_labels(issuable, raw_issuable)
+        # GH returns labels for issues but not for pull requests!
+        labels = if issuable.is_a?(MergeRequest)
+                   client.labels_for_issue(repo, raw_issuable.number)
+                 else
+                   raw_issuable.labels
+                 end
 
-        if issue.labels.count > 0
-          label_ids = issue.labels.map do |raw|
-            Label.find_by(LabelFormatter.new(project, raw).attributes).try(:id)
-          end
+        if labels.count > 0
+          label_ids = labels
+            .map { |attrs| @labels[attrs.name] }
+            .compact
 
           issuable.update_attribute(:label_ids, label_ids)
         end
       end
 
-      def import_comments(issuable)
-        comments = client.issue_comments(repo, issuable.iid, per_page: 100)
-        create_comments(issuable, comments)
+      def import_comments(issuable_type)
+        resource_type = "#{issuable_type}_comments".to_sym
+
+        # Two notes here:
+        # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note,
+        # compare it against every comment in the current imported page until we find match, and that's where start importing
+        # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns
+        # only comments on diffs, so select last note not based on noteable_type but on line_code
+        line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL'
+        last_note    = project.notes.where("line_code IS #{line_code_is}").last
+
+        fetch_resources(resource_type, repo, per_page: 100) do |comments|
+          if last_note
+            discard_inserted_comments(comments, last_note)
+            last_note = nil
+          end
+
+          create_comments(comments)
+        end
       end
 
-      def import_comments_on_diff(merge_request)
-        comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100)
-        create_comments(merge_request, comments)
+      def create_comments(comments)
+        ActiveRecord::Base.no_touching do
+          comments.each do |raw|
+            begin
+              comment         = CommentFormatter.new(project, raw)
+              # GH does not return info about comment's parent, so we guess it by checking its URL!
+              *_, parent, iid = URI(raw.html_url).path.split('/')
+              issuable_class = parent == 'issues' ? Issue : MergeRequest
+              issuable       = issuable_class.find_by_iid(iid)
+              next unless issuable
+
+              issuable.notes.create!(comment.attributes)
+            rescue => e
+              errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+            end
+          end
+        end
       end
 
-      def create_comments(issuable, comments)
-        comments.each do |raw|
-          comment = CommentFormatter.new(project, raw)
-          issuable.notes.create!(comment.attributes)
+      def discard_inserted_comments(comments, last_note)
+        last_note_attrs = nil
+
+        cut_off_index = comments.find_index do |raw|
+          comment           = CommentFormatter.new(project, raw)
+          comment_attrs     = comment.attributes
+          last_note_attrs ||= last_note.slice(*comment_attrs.keys)
+
+          comment_attrs.with_indifferent_access == last_note_attrs
         end
+
+        # No matching resource in the collection, which means we got halted right on the end of the last page, so all good
+        return unless cut_off_index
+
+        # Otherwise, remove the resources we've already inserted
+        comments.shift(cut_off_index + 1)
       end
 
       def import_wiki
-        unless project.wiki_enabled?
+        unless project.wiki.repository_exists?
           wiki = WikiFormatter.new(project)
           gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
-          project.update_attribute(:wiki_enabled, true)
         end
-
-        true
       rescue Gitlab::Shell::Error => e
         # GitHub error message when the wiki repo has not been created,
         # this means that repo has wiki enabled, but have no pages. So,
         # we can skip the import.
         if e.message !~ /repository not exported/
-          raise Projects::ImportService::Error, e.message
-        else
-          true
+          errors << { type: :wiki, errors: e.message }
+        end
+      end
+
+      def import_releases
+        fetch_resources(:releases, repo, per_page: 100) do |releases|
+          releases.each do |raw|
+            begin
+              gh_release = ReleaseFormatter.new(project, raw)
+              gh_release.create! if gh_release.valid?
+            rescue => e
+              errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+            end
+          end
+        end
+      end
+
+      def cache_labels!
+        project.labels.select(:id, :title).find_each do |label|
+          @labels[label.title] = label.id
         end
       end
+
+      def fetch_resources(resource_type, *opts)
+        return if imported?(resource_type)
+
+        opts.last.merge!(page: current_page(resource_type))
+
+        client.public_send(resource_type, *opts) do |resources|
+          yield resources
+          increment_page(resource_type)
+        end
+
+        imported!(resource_type)
+      end
+
+      def imported?(resource_type)
+        Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported")
+      end
+
+      def imported!(resource_type)
+        Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day)
+      end
+
+      def increment_page(resource_type)
+        key = "#{cache_key_prefix}:#{resource_type}:current-page"
+
+        # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is
+        # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around
+        page = Rails.cache.read(key)
+        page += 1
+        Rails.cache.write(key, page)
+
+        page
+      end
+
+      def current_page(resource_type)
+        Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 }
+      end
+
+      def cache_key_prefix
+        @cache_key_prefix ||= "github-import:#{project.id}"
+      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 835ec858b35c537dfd11c8c2146ea89fae2eec8e..8c32ac59fc5d2b84d3f7274aa598f92b9260b678 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -12,7 +12,7 @@ module Gitlab
           author_id: author_id,
           assignee_id: assignee_id,
           created_at: raw_data.created_at,
-          updated_at: updated_at
+          updated_at: raw_data.updated_at
         }
       end
 
@@ -20,8 +20,12 @@ module Gitlab
         raw_data.comments > 0
       end
 
-      def klass
-        Issue
+      def project_association
+        :issues
+      end
+
+      def find_condition
+        { iid: number }
       end
 
       def number
@@ -40,7 +44,7 @@ module Gitlab
 
       def assignee_id
         if assigned?
-          gl_user_id(raw_data.assignee.id)
+          gitlab_user_id(raw_data.assignee.id)
         end
       end
 
@@ -49,7 +53,7 @@ module Gitlab
       end
 
       def author_id
-        gl_user_id(raw_data.user.id) || project.creator_id
+        gitlab_author_id || project.creator_id
       end
 
       def body
@@ -57,7 +61,11 @@ module Gitlab
       end
 
       def description
-        @formatter.author_line(author) + body
+        if gitlab_author_id
+          body
+        else
+          formatter.author_line(author) + body
+        end
       end
 
       def milestone
@@ -69,10 +77,6 @@ module Gitlab
       def state
         raw_data.state == 'closed' ? 'closed' : 'opened'
       end
-
-      def updated_at
-        state == 'closed' ? raw_data.closed_at : raw_data.updated_at
-      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 9f18244e7d7a67ade7b56edcf184e8ea31d38ec8..211ccdc51bb09bb34c0c22b5c10f92105b05d9b9 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -9,8 +9,18 @@ module Gitlab
         }
       end
 
-      def klass
-        Label
+      def project_association
+        :labels
+      end
+
+      def create!
+        params  = attributes.except(:project)
+        service = ::Labels::FindOrCreateService.new(nil, project, params)
+        label   = service.execute(skip_authorization: true)
+
+        raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
+
+        label
       end
 
       private
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index 53d4b3102d195a40fb6cad8bb68d3e3898458279..401dd962521c4ee0f470d490ff448b26a5ac8dea 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -3,50 +3,30 @@ module Gitlab
     class MilestoneFormatter < BaseFormatter
       def attributes
         {
-          iid: number,
+          iid: raw_data.number,
           project: project,
-          title: title,
-          description: description,
-          due_date: due_date,
+          title: raw_data.title,
+          description: raw_data.description,
+          due_date: raw_data.due_on,
           state: state,
-          created_at: created_at,
-          updated_at: updated_at
+          created_at: raw_data.created_at,
+          updated_at: raw_data.updated_at
         }
       end
 
-      def klass
-        Milestone
+      def project_association
+        :milestones
       end
 
-      private
-
-      def number
-        raw_data.number
-      end
-
-      def title
-        raw_data.title
-      end
-
-      def description
-        raw_data.description
+      def find_condition
+        { iid: raw_data.number }
       end
 
-      def due_date
-        raw_data.due_on
-      end
+      private
 
       def state
         raw_data.state == 'closed' ? 'closed' : 'active'
       end
-
-      def created_at
-        raw_data.created_at
-      end
-
-      def updated_at
-        state == 'closed' ? raw_data.closed_at : raw_data.updated_at
-      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index f4221003db5119a171c1a69921cbb4f9a4f15615..a241006884522f5fabc2a102c8dbbdf72195c26c 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,10 +1,11 @@
 module Gitlab
   module GithubImport
     class ProjectCreator
-      attr_reader :repo, :namespace, :current_user, :session_data
+      attr_reader :repo, :name, :namespace, :current_user, :session_data
 
-      def initialize(repo, namespace, current_user, session_data)
+      def initialize(repo, name, namespace, current_user, session_data)
         @repo = repo
+        @name = name
         @namespace = namespace
         @current_user = current_user
         @session_data = session_data
@@ -13,17 +14,36 @@ module Gitlab
       def execute
         ::Projects::CreateService.new(
           current_user,
-          name: repo.name,
-          path: repo.name,
+          name: name,
+          path: name,
           description: repo.description,
           namespace_id: namespace.id,
-          visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
+          visibility_level: visibility_level,
           import_type: "github",
           import_source: repo.full_name,
-          import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
-          wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
+          import_url: import_url,
+          skip_wiki: skip_wiki
         ).execute
       end
+
+      private
+
+      def import_url
+        repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
+      end
+
+      def visibility_level
+        repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+      end
+
+      #
+      # If the GitHub project repository has wiki, we should not create the
+      # default wiki. Otherwise the GitHub importer will fail because the wiki
+      # repository already exist.
+      #
+      def skip_wiki
+        repo.has_wiki?
+      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index b84538a090a268a0e97af332dd2528579941ecb1..b9a227fb11a5d13040549327da0a41933e2f0f55 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -20,12 +20,16 @@ module Gitlab
           author_id: author_id,
           assignee_id: assignee_id,
           created_at: raw_data.created_at,
-          updated_at: updated_at
+          updated_at: raw_data.updated_at
         }
       end
 
-      def klass
-        MergeRequest
+      def project_association
+        :merge_requests
+      end
+
+      def find_condition
+        { iid: number }
       end
 
       def number
@@ -56,6 +60,10 @@ module Gitlab
         end
       end
 
+      def url
+        raw_data.url
+      end
+
       private
 
       def assigned?
@@ -64,7 +72,7 @@ module Gitlab
 
       def assignee_id
         if assigned?
-          gl_user_id(raw_data.assignee.id)
+          gitlab_user_id(raw_data.assignee.id)
         end
       end
 
@@ -73,7 +81,7 @@ module Gitlab
       end
 
       def author_id
-        gl_user_id(raw_data.user.id) || project.creator_id
+        gitlab_author_id || project.creator_id
       end
 
       def body
@@ -81,7 +89,11 @@ module Gitlab
       end
 
       def description
-        formatter.author_line(author) + body
+        if gitlab_author_id
+          body
+        else
+          formatter.author_line(author) + body
+        end
       end
 
       def milestone
@@ -99,15 +111,6 @@ module Gitlab
                      'opened'
                    end
       end
-
-      def updated_at
-        case state
-        when 'merged' then raw_data.merged_at
-        when 'closed' then raw_data.closed_at
-        else
-          raw_data.updated_at
-        end
-      end
     end
   end
 end
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1ad702a6058701baa5d154774f315bd1268a5f6d
--- /dev/null
+++ b/lib/gitlab/github_import/release_formatter.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module GithubImport
+    class ReleaseFormatter < BaseFormatter
+      def attributes
+        {
+          project: project,
+          tag: raw_data.tag_name,
+          description: raw_data.body,
+          created_at: raw_data.created_at,
+          updated_at: raw_data.created_at
+        }
+      end
+
+      def project_association
+        :releases
+      end
+
+      def find_condition
+        { tag: raw_data.tag_name }
+      end
+
+      def valid?
+        !raw_data.draft
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 46d40f75be6b93fa503ec47e9b4253dd555c87ed..e44d7934fda6f0603b25ab557637e5b120b49e93 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -41,7 +41,8 @@ module Gitlab
               title: issue["title"],
               state: issue["state"],
               updated_at: issue["updated_at"],
-              author_id: gl_user_id(project, issue["author"]["id"])
+              author_id: gitlab_user_id(project, issue["author"]["id"]),
+              confidential: issue["confidential"]
             )
           end
         end
@@ -51,7 +52,7 @@ module Gitlab
 
       private
 
-      def gl_user_id(project, gitlab_id)
+      def gitlab_user_id(project, gitlab_id)
         user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s)
         (user && user.id) || project.creator_id
       end
diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb
deleted file mode 100644
index 8d0132a744cac5ca1848541c6c01404e6125b0d7..0000000000000000000000000000000000000000
--- a/lib/gitlab/gitorious_import.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Gitlab
-  module GitoriousImport
-    GITORIOUS_HOST = "https://gitorious.org"
-  end
-end
diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb
deleted file mode 100644
index 99fe5bdebfcf1a4cdf26b59373ee9a606bc62c19..0000000000000000000000000000000000000000
--- a/lib/gitlab/gitorious_import/client.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Gitlab
-  module GitoriousImport
-    class Client
-      attr_reader :repo_list
-
-      def initialize(repo_list)
-        @repo_list = repo_list
-      end
-
-      def authorize_url(redirect_uri)
-        "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}"
-      end
-
-      def repos
-        @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
-      end
-
-      def repo(id)
-        repos.find { |repo| repo.id == id }
-      end
-
-      private
-
-      def repo_names
-        repo_list.to_s.split(',').map(&:strip).reject(&:blank?)
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
deleted file mode 100644
index 8e22aa9286ddf4d2a332f2daca87a6ab5f2eee9d..0000000000000000000000000000000000000000
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
-  module GitoriousImport
-    class ProjectCreator
-      attr_reader :repo, :namespace, :current_user
-
-      def initialize(repo, namespace, current_user)
-        @repo = repo
-        @namespace = namespace
-        @current_user = current_user
-      end
-
-      def execute
-        ::Projects::CreateService.new(
-          current_user,
-          name: repo.name,
-          path: repo.path,
-          description: repo.description,
-          namespace_id: namespace.id,
-          visibility_level: Gitlab::VisibilityLevel::PUBLIC,
-          import_type: "gitorious",
-          import_source: repo.full_name,
-          import_url: repo.import_url
-        ).execute
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb
deleted file mode 100644
index c88f1ae358d1ce61b9ea95c84707577ad1c458ac..0000000000000000000000000000000000000000
--- a/lib/gitlab/gitorious_import/repository.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Gitlab
-  module GitoriousImport
-    Repository = Struct.new(:full_name) do
-      def id
-        Digest::SHA1.hexdigest(full_name)
-      end
-
-      def namespace
-        segments.first
-      end
-
-      def path
-        segments.last
-      end
-
-      def name
-        path.titleize
-      end
-
-      def description
-        ""
-      end
-
-      def import_url
-        "#{GITORIOUS_HOST}/#{full_name}.git"
-      end
-
-      private
-
-      def segments
-        full_name.split('/')
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index c5a11148d33f7c5dae8fa1c8b5811b91a177b9b7..2c21804fe7a6328b2f1d027b0dcacc918119d753 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -11,7 +11,6 @@ module Gitlab
 
       if current_user
         gon.current_user_id = current_user.id
-        gon.api_token = current_user.private_token
       end
     end
   end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 62da327931faff96264b4152325c321bc7f16281..1f4edc369288060fb5f75231783e259417ba5bf9 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -92,19 +92,17 @@ module Gitlab
           end
 
           issue = Issue.create!(
-            project_id:   project.id,
-            title:        raw_issue["title"],
-            description:  body,
-            author_id:    project.creator_id,
-            assignee_id:  assignee_id,
-            state:        raw_issue["state"] == "closed" ? "closed" : "opened"
+            iid:         raw_issue['id'],
+            project_id:  project.id,
+            title:       raw_issue['title'],
+            description: body,
+            author_id:   project.creator_id,
+            assignee_id: assignee_id,
+            state:       raw_issue['state'] == 'closed' ? 'closed' : 'opened'
           )
 
-          issue.add_labels_by_names(labels)
-
-          if issue.iid != raw_issue["id"]
-            issue.update_attribute(:iid, raw_issue["id"])
-          end
+          issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
+          issue.update_attribute(:label_ids, issue_labels.pluck(:id))
 
           import_issue_comments(issue, comments)
         end
@@ -236,8 +234,8 @@ module Gitlab
       end
 
       def create_label(name)
-        color = nice_label_color(name)
-        Label.create!(project_id: project.id, name: name, color: color)
+        params = { name: name, color: nice_label_color(name) }
+        ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
       end
 
       def format_content(raw_content)
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 3e5d728f3bce43ecc99e37db99895f17ee7c0f74..f8809db21aa9a3b3e4a9348c69cdcbda464462b4 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -5,19 +5,61 @@ module Gitlab
     def identify(identifier, project, newrev)
       if identifier.blank?
         # Local push from gitlab
-        email = project.commit(newrev).author_email rescue nil
-        User.find_by(email: email) if email
-
+        identify_using_commit(project, newrev)
       elsif identifier =~ /\Auser-\d+\Z/
         # git push over http
-        user_id = identifier.gsub("user-", "")
-        User.find_by(id: user_id)
-
+        identify_using_user(identifier)
       elsif identifier =~ /\Akey-\d+\Z/
         # git push over ssh
-        key_id = identifier.gsub("key-", "")
-        Key.find_by(id: key_id).try(:user)
+        identify_using_ssh_key(identifier)
+      end
+    end
+
+    # Tries to identify a user based on a commit SHA.
+    def identify_using_commit(project, ref)
+      commit = project.commit(ref)
+
+      return if !commit || !commit.author_email
+
+      email = commit.author_email
+
+      identify_with_cache(:email, email) do
+        User.find_by(email: email)
       end
     end
+
+    # Tries to identify a user based on a user identifier (e.g. "user-123").
+    def identify_using_user(identifier)
+      user_id = identifier.gsub("user-", "")
+
+      identify_with_cache(:user, user_id) do
+        User.find_by(id: user_id)
+      end
+    end
+
+    # Tries to identify a user based on an SSH key identifier (e.g. "key-123").
+    def identify_using_ssh_key(identifier)
+      key_id = identifier.gsub("key-", "")
+
+      identify_with_cache(:ssh_key, key_id) do
+        User.find_by_ssh_key_id(key_id)
+      end
+    end
+
+    def identify_with_cache(category, key)
+      if identification_cache[category].key?(key)
+        identification_cache[category][key]
+      else
+        identification_cache[category][key] = yield
+      end
+    end
+
+    def identification_cache
+      @identification_cache ||= {
+        email: {},
+        user: {},
+        ssh_key: {}
+      }
+    end
   end
 end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index bb562bdcd2c0fb8df35d3b3a98c4d18058c1e27a..eb667a85b78fc390709d38613295515934272624 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -2,7 +2,8 @@ module Gitlab
   module ImportExport
     extend self
 
-    VERSION = '0.1.3'
+    # For every version update, the version history in import_export.md has to be kept up to date.
+    VERSION = '0.1.5'
     FILENAME_LIMIT = 50
 
     def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34169319b263cf115c6116d2cd8f935985f9fcac
--- /dev/null
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -0,0 +1,28 @@
+module Gitlab
+  module ImportExport
+    class AttributeCleaner
+      ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
+
+      def self.clean(*args)
+        new(*args).clean
+      end
+
+      def initialize(relation_hash:, relation_class:)
+        @relation_hash = relation_hash
+        @relation_class = relation_class
+      end
+
+      def clean
+        @relation_hash.reject do |key, _value|
+          prohibited_key?(key) || !@relation_class.attribute_method?(key)
+        end.except('id')
+      end
+
+      private
+
+      def prohibited_key?(key)
+        key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index e522a0fc8f69a6ba1b906bdf9124458bda24f6ba..f00c7460e82abba81710514b1da68c866c60dd71 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -1,6 +1,8 @@
 module Gitlab
   module ImportExport
     module CommandLineUtil
+      DEFAULT_MODE = 0700
+
       def tar_czf(archive:, dir:)
         tar_with_options(archive: archive, dir: dir, options: 'czf')
       end
@@ -21,6 +23,11 @@ module Gitlab
         execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
       end
 
+      def mkdir_p(path)
+        FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
+        FileUtils.chmod(DEFAULT_MODE, path)
+      end
+
       private
 
       def tar_with_options(archive:, dir:, options:)
@@ -45,7 +52,7 @@ module Gitlab
         # if we are copying files, create the destination folder
         destination_folder = File.file?(source) ? File.dirname(destination) : destination
 
-        FileUtils.mkdir_p(destination_folder)
+        mkdir_p(destination_folder)
         FileUtils.copy_entry(source, destination)
         true
       end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index eca6e5b6d512dcfc358b4164b4f2e8f1a9c53a9c..ffd17118c91a2112b41f011db6fc2289d6e0cb59 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -15,7 +15,7 @@ module Gitlab
       end
 
       def import
-        FileUtils.mkdir_p(@shared.export_path)
+        mkdir_p(@shared.export_path)
 
         wait_for_archived_file do
           decompress_archive
@@ -43,6 +43,14 @@ module Gitlab
 
         raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
 
+        remove_symlinks!
+      end
+
+      def remove_symlinks!
+        Dir["#{@shared.export_path}/**/*"].each do |path|
+          FileUtils.rm(path) if File.lstat(path).symlink?
+        end
+
         true
       end
     end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 1da51043611a4d547da37db26133362771e74bb6..e6ecd11860999cdef08ad2c2d9b217419fa47249 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,15 +1,21 @@
 # Model relationships to be included in the project import/export
 project_tree:
+  - labels:
+      :priorities
+  - milestones:
+    - :events
   - issues:
     - :events
     - notes:
       - :author
       - :events
     - label_links:
-      - :label
+      - label:
+          :priorities
     - milestone:
       - :events
   - snippets:
+    - :award_emoji
     - notes:
         :author
   - :releases
@@ -22,7 +28,8 @@ project_tree:
     - :merge_request_diff
     - :events
     - label_links:
-      - :label
+      - label:
+          :priorities
     - milestone:
       - :events
   - pipelines:
@@ -35,19 +42,15 @@ project_tree:
   - :deploy_keys
   - :services
   - :hooks
-  - :protected_branches
-  - :labels
-  - milestones:
-    - :events
+  - protected_branches:
+    - :merge_access_levels
+    - :push_access_levels
+  - :project_feature
 
 # Only include the following attributes for the models specified.
 included_attributes:
   project:
     - :description
-    - :issues_enabled
-    - :merge_requests_enabled
-    - :wiki_enabled
-    - :snippets_enabled
     - :visibility_level
     - :archived
   user:
@@ -67,9 +70,17 @@ excluded_attributes:
     - :milestone_id
   merge_requests:
     - :milestone_id
+  award_emoji:
+    - :awardable_id
 
 methods:
+  labels:
+    - :type
+  label:
+    - :type
   statuses:
     - :type
+  services:
+    - :type
   merge_request_diff:
-    - :utf8_st_diffs
\ No newline at end of file
+    - :utf8_st_diffs
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 0cc10f4008712da59483bea5ce4efba8922a8cf6..48c09dafcb6b76dc02e3c3f6dd845827d53559f2 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -65,11 +65,17 @@ module Gitlab
       # +value+ existing model to be included in the hash
       # +parsed_hash+ the original hash
       def parse_hash(value)
+        return nil if already_contains_methods?(value)
+
         @attributes_finder.parse(value) do |hash|
           { include: hash_or_merge(value, hash) }
         end
       end
 
+      def already_contains_methods?(value)
+        value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
+      end
+
       # Adds new model configuration to an existing hash with key +current_key+
       # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
       #
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 36c4cf6efa0493deef2d098cdb3f625bf96902e1..b790733f4a75a3bba78db43504aaadd1bd1420bd 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -55,7 +55,12 @@ module Gitlab
       end
 
       def member_hash(member)
-        member.except('id').merge(source_id: @project.id, importing: true)
+        parsed_hash(member).merge('source_id' => @project.id, 'importing' => true)
+      end
+
+      def parsed_hash(member)
+        Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys,
+                                                     relation_class: ProjectMember)
       end
 
       def find_project_user_query(member)
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c7b3551b84c5cfdbf9cc081f7cd9bdff24389f72..c551321c18dbb3aeb879cdc37138e40941bddb70 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -9,8 +9,14 @@ module Gitlab
       end
 
       def restore
-        json = IO.read(@path)
-        @tree_hash = ActiveSupport::JSON.decode(json)
+        begin
+          json = IO.read(@path)
+          @tree_hash = ActiveSupport::JSON.decode(json)
+        rescue => e
+          Rails.logger.error("Import/Export error: #{e.message}")
+          raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
+        end
+
         @project_members = @tree_hash.delete('project_members')
 
         ActiveRecord::Base.no_touching do
@@ -61,11 +67,17 @@ module Gitlab
       def restore_project
         return @project unless @tree_hash
 
-        project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
         @project.update(project_params)
         @project
       end
 
+      def project_params
+        @tree_hash.reject do |key, value|
+          # return params that are not 1 to many or 1 to 1 relations
+          value.is_a?(Array) || key == key.singularize
+        end
+      end
+
       # Given a relation hash containing one or more models and its relationships,
       # loops through each model and each object from a model type and
       # and assigns its correspondent attributes hash from +tree_hash+
@@ -104,13 +116,18 @@ module Gitlab
       def create_relation(relation, relation_hash_list)
         relation_array = [relation_hash_list].flatten.map do |relation_hash|
           Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
-                                                       relation_hash: relation_hash.merge('project_id' => restored_project.id),
+                                                       relation_hash: parsed_relation_hash(relation_hash),
                                                        members_mapper: members_mapper,
-                                                       user: @user)
+                                                       user: @user,
+                                                       project_id: restored_project.id)
         end
 
         relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
       end
+
+      def parsed_relation_hash(relation_hash)
+        relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
+      end
     end
   end
 end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 9153088e966dc9604803c914494b9ec595caa61f..2fbf437ec262491c4a8b4f6029528c40fc661e1c 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -1,6 +1,8 @@
 module Gitlab
   module ImportExport
     class ProjectTreeSaver
+      include Gitlab::ImportExport::CommandLineUtil
+
       attr_reader :full_path
 
       def initialize(project:, shared:)
@@ -10,7 +12,7 @@ module Gitlab
       end
 
       def save
-        FileUtils.mkdir_p(@shared.export_path)
+        mkdir_p(@shared.export_path)
 
         File.write(full_path, project_json_tree)
         true
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index b0726268ca617ce6f6807eeb5afef80d38f8da10..a0e80fccad9295c903e2fef142ebecc77251f3e7 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -7,23 +7,30 @@ module Gitlab
                     variables: 'Ci::Variable',
                     triggers: 'Ci::Trigger',
                     builds: 'Ci::Build',
-                    hooks: 'ProjectHook' }.freeze
+                    hooks: 'ProjectHook',
+                    merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
+                    push_access_levels: 'ProtectedBranch::PushAccessLevel',
+                    labels: :project_labels,
+                    priorities: :label_priorities,
+                    label: :project_label }.freeze
 
-      USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+      USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze
+
+      PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
 
       BUILD_MODELS = %w[Ci::Build commit_status].freeze
 
       IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
 
-      EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
+      EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
 
       def self.create(*args)
         new(*args).create
       end
 
-      def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+      def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
         @relation_name = OVERRIDES[relation_sym] || relation_sym
-        @relation_hash = relation_hash.except('id', 'noteable_id')
+        @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id)
         @members_mapper = members_mapper
         @user = user
         @imported_object_retries = 0
@@ -50,6 +57,8 @@ module Gitlab
 
         update_user_references
         update_project_references
+
+        handle_group_label if group_label?
         reset_ci_tokens if @relation_name == 'Ci::Trigger'
         @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
         set_st_diffs if @relation_name == :merge_request_diff
@@ -117,6 +126,20 @@ module Gitlab
         @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
       end
 
+      def group_label?
+        @relation_hash['type'] == 'GroupLabel'
+      end
+
+      def handle_group_label
+        # If there's no group, move the label to a project label
+        if @relation_hash['group_id']
+          @relation_hash['project_id'] = nil
+          @relation_name = :group_label
+        else
+          @relation_hash['type'] = 'ProjectLabel'
+        end
+      end
+
       def reset_ci_tokens
         return unless Gitlab::ImportExport.reset_tokens?
 
@@ -149,7 +172,8 @@ module Gitlab
       end
 
       def parsed_relation_hash
-        @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+        @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
+                                                                               relation_class: relation_class)
       end
 
       def set_st_diffs
@@ -161,14 +185,36 @@ module Gitlab
         # Otherwise always create the record, skipping the extra SELECT clause.
         @existing_or_new_object ||= begin
           if EXISTING_OBJECT_CHECK.include?(@relation_name)
-            existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id'))
-            existing_object.assign_attributes(parsed_relation_hash)
+            attribute_hash = attribute_hash_for(['events', 'priorities'])
+
+            existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
+
             existing_object
           else
             relation_class.new(parsed_relation_hash)
           end
         end
       end
+
+      def attribute_hash_for(attributes)
+        attributes.inject({}) do |hash, value|
+          hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
+          hash
+        end
+      end
+
+      def existing_object
+        @existing_object ||=
+          begin
+            finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+            finder_hash = parsed_relation_hash.slice(*finder_attributes)
+            existing_object = relation_class.find_or_create_by(finder_hash)
+            # Done in two steps, as MySQL behaves differently than PostgreSQL using
+            # the +find_or_create_by+ method and does not return the ID the second time.
+            existing_object.update!(parsed_relation_hash)
+            existing_object
+          end
+      end
     end
   end
 end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 6d9379acf25869458600edb6d4daf552a22fbd93..48a9a6fa5e2c8b88b726c75861a4c8fba08cb409 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -12,7 +12,7 @@ module Gitlab
       def restore
         return true unless File.exist?(@path_to_bundle)
 
-        FileUtils.mkdir_p(path_to_repo)
+        mkdir_p(path_to_repo)
 
         git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
       rescue => e
@@ -22,10 +22,6 @@ module Gitlab
 
       private
 
-      def repos_path
-        Gitlab.config.gitlab_shell.repos_path
-      end
-
       def path_to_repo
         @project.repository.path_to_repo
       end
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 331e14021e6831ef388720ede3abcc99d65791a3..a7028a32570c581a6c1d2331b092907de385b957 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -20,7 +20,7 @@ module Gitlab
       private
 
       def bundle_to_disk
-        FileUtils.mkdir_p(@shared.export_path)
+        mkdir_p(@shared.export_path)
         git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
       rescue => e
         @shared.error(e)
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index de3fe6d822e0273ac15d4295be47245365247635..bd3c3ee3b2fa8835f2d893a941ef4febc614467e 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -24,12 +24,19 @@ module Gitlab
       end
 
       def verify_version!(version)
-        if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
-          raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+        if different_version?(version)
+          raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
         else
           true
         end
       end
+
+      def different_version?(version)
+        Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
+      rescue => e
+        Rails.logger.error("Import/Export error: #{e.message}")
+        raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
+      end
     end
   end
 end
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
index 9b642d740b7c6087e5fe20b490337bae2616b4b6..7cf88298642760a8a2c1d3f87f6dc447a676771f 100644
--- a/lib/gitlab/import_export/version_saver.rb
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -1,12 +1,14 @@
 module Gitlab
   module ImportExport
     class VersionSaver
+      include Gitlab::ImportExport::CommandLineUtil
+
       def initialize(shared:)
         @shared = shared
       end
 
       def save
-        FileUtils.mkdir_p(@shared.export_path)
+        mkdir_p(@shared.export_path)
 
         File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
       rescue => e
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 6107420e4dd79ab6bb8b7d1d60b72e2111877e21..1e6722a7bba6e5ae9ad3372376f4e00cfda619b7 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -9,7 +9,7 @@ module Gitlab
       end
 
       def bundle_to_disk(full_path)
-        FileUtils.mkdir_p(@shared.export_path)
+        mkdir_p(@shared.export_path)
         git_bundle(repo_path: path_to_repo, bundle_path: full_path)
       rescue => e
         @shared.error(e)
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 59a05411fe9dc36395087ced47fb0ecc3e9bfd59..94261b7eeeded0b6e1760473f647c6e3d899a1bd 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -14,13 +14,12 @@ module Gitlab
 
       def options
         {
-          'GitHub'          => 'github',
-          'Bitbucket'       => 'bitbucket',
-          'GitLab.com'      => 'gitlab',
-          'Gitorious.org'   => 'gitorious',
-          'Google Code'     => 'google_code',
-          'FogBugz'         => 'fogbugz',
-          'Repo by URL'     => 'git',
+          'GitHub'        => 'github',
+          'Bitbucket'     => 'bitbucket',
+          'GitLab.com'    => 'gitlab',
+          'Google Code'   => 'google_code',
+          'FogBugz'       => 'fogbugz',
+          'Repo by URL'   => 'git',
           'GitLab export' => 'gitlab_project'
         }
       end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index d7be50bd43725d8cee76cf04299b0cc3d7b80308..801dfde9a368f90edefa2f7d20331c6ec670e2cf 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,5 +1,7 @@
 module Gitlab
   module IncomingEmail
+    WILDCARD_PLACEHOLDER = '%{key}'.freeze
+
     class << self
       FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
 
@@ -7,8 +9,16 @@ module Gitlab
         config.enabled && config.address
       end
 
+      def supports_wildcard?
+        config.address && config.address.include?(WILDCARD_PLACEHOLDER)
+      end
+
+      def supports_issue_creation?
+        enabled? && supports_wildcard?
+      end
+
       def reply_address(key)
-        config.address.gsub('%{key}', key)
+        config.address.gsub(WILDCARD_PLACEHOLDER, key)
       end
 
       def key_from_address(address)
diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb
index 1bec60882925b8d3a129df8676a2b0389ac08ef3..b8ca7f2f55fc010733c11dbcc71fb6e4ed71d683 100644
--- a/lib/gitlab/issues_labels.rb
+++ b/lib/gitlab/issues_labels.rb
@@ -18,8 +18,8 @@ module Gitlab
           { title: "enhancement", color: green }
         ]
 
-        labels.each do |label|
-          project.labels.create(label)
+        labels.each do |params|
+          ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
         end
       end
     end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 2f326d00a2f4b37188509e077cc9a18270fc70ff..7e06bd2b0fb5c7c3b4debcd0c1067392f995bb61 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -51,8 +51,6 @@ module Gitlab
           user.ldap_block
           false
         end
-      rescue
-        false
       end
 
       def adapter
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 9a5bcfb5c9bab9f8df436219f30f8a95a58b903a..8b38cfaefb6049a7582f28b94fc1d874a163c55a 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -23,31 +23,7 @@ module Gitlab
       end
 
       def users(field, value, limit = nil)
-        if field.to_sym == :dn
-          options = {
-            base: value,
-            scope: Net::LDAP::SearchScope_BaseObject
-          }
-        else
-          options = {
-            base: config.base,
-            filter: Net::LDAP::Filter.eq(field, value)
-          }
-        end
-
-        if config.user_filter.present?
-          user_filter = Net::LDAP::Filter.construct(config.user_filter)
-
-          options[:filter] = if options[:filter]
-                               Net::LDAP::Filter.join(options[:filter], user_filter)
-                             else
-                               user_filter
-                             end
-        end
-
-        if limit.present?
-          options.merge!(size: limit)
-        end
+        options = user_options(field, value, limit)
 
         entries = ldap_search(options).select do |entry|
           entry.respond_to? config.uid
@@ -86,10 +62,49 @@ module Gitlab
             results
           end
         end
+      rescue Net::LDAP::Error => error
+        Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
+        []
       rescue Timeout::Error
         Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
         []
       end
+
+      private
+
+      def user_options(field, value, limit)
+        options = { attributes: user_attributes }
+        options[:size] = limit if limit
+
+        if field.to_sym == :dn
+          options[:base] = value
+          options[:scope] = Net::LDAP::SearchScope_BaseObject
+          options[:filter] = user_filter
+        else
+          options[:base] = config.base
+          options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value))
+        end
+
+        options
+      end
+
+      def user_filter(filter = nil)
+        if config.user_filter.present?
+          user_filter = Net::LDAP::Filter.construct(config.user_filter)
+        end
+
+        if user_filter && filter
+          Net::LDAP::Filter.join(filter, user_filter)
+        elsif user_filter
+          user_filter
+        else
+          filter
+        end
+      end
+
+      def user_attributes
+        %W(#{config.uid} cn mail dn)
+      end
     end
   end
 end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index f9bb577532309f07cca8b0dfd8ed555b6c4f2300..6ea069d26df25636634a4eac412c2c64c7b76f20 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -92,6 +92,10 @@ module Gitlab
         options['timeout'].to_i
       end
 
+      def has_auth?
+        options['password'] || options['bind_dn']
+      end
+
       protected
 
       def base_config
@@ -122,10 +126,6 @@ module Gitlab
           }
         }
       end
-
-      def has_auth?
-        options['password'] || options['bind_dn']
-      end
     end
   end
 end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5f67e97fa2a718ad0ff8e004c281834d2b7d94f7
--- /dev/null
+++ b/lib/gitlab/lfs_token.rb
@@ -0,0 +1,48 @@
+module Gitlab
+  class LfsToken
+    attr_accessor :actor
+
+    TOKEN_LENGTH = 50
+    EXPIRY_TIME = 1800
+
+    def initialize(actor)
+      @actor =
+        case actor
+        when DeployKey, User
+          actor
+        when Key
+          actor.user
+        else
+          raise 'Bad Actor'
+        end
+    end
+
+    def token
+      Gitlab::Redis.with do |redis|
+        token = redis.get(redis_key)
+        token ||= Devise.friendly_token(TOKEN_LENGTH)
+        redis.set(redis_key, token, ex: EXPIRY_TIME)
+
+        token
+      end
+    end
+
+    def user?
+      actor.is_a?(User)
+    end
+
+    def type
+      actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+    end
+
+    def actor_name
+      actor.is_a?(User) ? actor.username : "lfs+deploy-key-#{actor.id}"
+    end
+
+    private
+
+    def redis_key
+      "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}" if actor
+    end
+  end
+end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 12999a90a298fa78ef7c31ca5c9a1c6398199641..a5220d92312c559305a771475f6e1f45d3b91f51 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -33,7 +33,12 @@ module Gitlab
         config[:mailbox] = 'inbox' if config[:mailbox].nil?
 
         if config[:enabled] && config[:address]
-          config[:redis_url] = Gitlab::Redis.new(rails_env).url
+          gitlab_redis = Gitlab::Redis.new(rails_env)
+          config[:redis_url] = gitlab_redis.url
+
+          if gitlab_redis.sentinels?
+            config[:sentinels] = gitlab_redis.sentinels
+          end
         end
 
         config
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 41fcd971c228c1341ba807c24e389d86d2a2c3c9..3d1ba33ec68d69378450449758b211fe1b2a7509 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -124,6 +124,15 @@ module Gitlab
       trans.action = action if trans
     end
 
+    # Tracks an event.
+    #
+    # See `Gitlab::Metrics::Transaction#add_event` for more details.
+    def self.add_event(*args)
+      trans = current_transaction
+
+      trans.add_event(*args) if trans
+    end
+
     # Returns the prefix to use for the name of a series.
     def self.series_prefix
       @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_'
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index f23d67e1e3849f4ab512c2d390110605009988d9..bd0afe53c5166da5abc2515a57f0a8d82d64fb8d 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,15 +4,20 @@ module Gitlab
     class Metric
       JITTER_RANGE = 0.000001..0.001
 
-      attr_reader :series, :values, :tags
+      attr_reader :series, :values, :tags, :type
 
       # series - The name of the series (as a String) to store the metric in.
       # values - A Hash containing the values to store.
       # tags   - A Hash containing extra tags to add to the metrics.
-      def initialize(series, values, tags = {})
+      def initialize(series, values, tags = {}, type = :metric)
         @values = values
         @series = series
         @tags   = tags
+        @type   = type
+      end
+
+      def event?
+        type == :event
       end
 
       # Returns a Hash in a format that can be directly written to InfluxDB.
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index e61670f491cfd5d8b58f1366fa834b3680aefe99..01c96a6fe960f223a7fce9b836291a83d39fd328 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -4,6 +4,17 @@ module Gitlab
     class RackMiddleware
       CONTROLLER_KEY = 'action_controller.instance'
       ENDPOINT_KEY   = 'api.endpoint'
+      CONTENT_TYPES = {
+        'text/html' => :html,
+        'text/plain' => :txt,
+        'application/json' => :json,
+        'text/js' => :js,
+        'application/atom+xml' => :atom,
+        'image/png' => :png,
+        'image/jpeg' => :jpeg,
+        'image/gif' => :gif,
+        'image/svg+xml' => :svg
+      }
 
       def initialize(app)
         @app = app
@@ -17,6 +28,10 @@ module Gitlab
         begin
           retval = trans.run { @app.call(env) }
 
+        rescue Exception => error # rubocop: disable Lint/RescueException
+          trans.add_event(:rails_exception)
+
+          raise error
         # Even in the event of an error we want to submit any metrics we
         # might've gathered up to this point.
         ensure
@@ -42,8 +57,15 @@ module Gitlab
       end
 
       def tag_controller(trans, env)
-        controller   = env[CONTROLLER_KEY]
-        trans.action = "#{controller.class.name}##{controller.action_name}"
+        controller = env[CONTROLLER_KEY]
+        action = "#{controller.class.name}##{controller.action_name}"
+        suffix = CONTENT_TYPES[controller.content_type]
+
+        if suffix && suffix != :html
+          action += ".#{suffix}"
+        end
+
+        trans.action = action
       end
 
       def tag_endpoint(trans, env)
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index a1240fd33eee739d2e62990f72e161f5cb6a0c13..f9dd8e419128e1330bed49c974779cb29775fb4c 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,6 +11,10 @@ module Gitlab
           # Old gitlad-shell messages don't provide enqueued_at/created_at attributes
           trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
           trans.run { yield }
+        rescue Exception => error # rubocop: disable Lint/RescueException
+          trans.add_event(:sidekiq_exception)
+
+          raise error
         ensure
           trans.finish
         end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 968f32189505326c02c3c32a5a08498b83e46875..7bc16181be624cac25ed051d8b27b94a20a04440 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,10 @@ module Gitlab
     class Transaction
       THREAD_KEY = :_gitlab_metrics_transaction
 
-      attr_reader :tags, :values, :methods
+      # The series to store events (e.g. Git pushes) in.
+      EVENT_SERIES = 'events'
+
+      attr_reader :tags, :values, :method, :metrics
 
       attr_accessor :action
 
@@ -55,6 +58,20 @@ module Gitlab
         @metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags)
       end
 
+      # Tracks a business level event
+      #
+      # Business level events including events such as Git pushes, Emails being
+      # sent, etc.
+      #
+      # event_name - The name of the event (e.g. "git_push").
+      # tags - A set of tags to attach to the event.
+      def add_event(event_name, tags = {})
+        @metrics << Metric.new(EVENT_SERIES,
+                               { count: 1 },
+                               { event: event_name }.merge(tags),
+                               :event)
+      end
+
       # Returns a MethodCall object for the given name.
       def method_call_for(name)
         unless method = @methods[name]
@@ -101,7 +118,7 @@ module Gitlab
         submit_hashes = submit.map do |metric|
           hash = metric.to_hash
 
-          hash[:tags][:action] ||= @action if @action
+          hash[:tags][:action] ||= @action if @action && !metric.event?
 
           hash
         end
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 56608b1b2765dd18b9c480d88d2b9689b503dd39..5d2d7d0026cd6273c31fbfb6bf48b3f4555e7bbf 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -11,7 +11,7 @@ module Gitlab
       
       def call(env)
         trans = Gitlab::Metrics.current_transaction
-        proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence
+        proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
         if trans && proxy_start
           # Time in milliseconds since gitlab-workhorse started the request
           trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
new file mode 100644
index 0000000000000000000000000000000000000000..879d46446b398e83eb0ea8374970e97cd3313562
--- /dev/null
+++ b/lib/gitlab/optimistic_locking.rb
@@ -0,0 +1,19 @@
+module Gitlab
+  module OptimisticLocking
+    extend self
+
+    def retry_lock(subject, retries = 100, &block)
+      loop do
+        begin
+          ActiveRecord::Base.transaction do
+            return block.call(subject)
+          end
+        rescue ActiveRecord::StaleObjectError
+          retries -= 1
+          raise unless retries >= 0
+          subject.reload
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index ca23ccef25bb23ce236764a3463951c6d3494b9c..cc74bb29087c7fd56cc0d7d07b3f57131fd5af53 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -18,18 +18,18 @@ module Gitlab
         FileUtils.mkdir_p(path)
       end
 
-      @cmd_output = ""
-      @cmd_status = 0
+      cmd_output = ""
+      cmd_status = 0
       Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
-        # We are not using stdin so we should close it, in case the command we
-        # are running waits for input.
+        yield(stdin) if block_given?
         stdin.close
-        @cmd_output << stdout.read
-        @cmd_output << stderr.read
-        @cmd_status = wait_thr.value.exitstatus
+
+        cmd_output << stdout.read
+        cmd_output << stderr.read
+        cmd_status = wait_thr.value.exitstatus
       end
 
-      [@cmd_output, @cmd_status]
+      [cmd_output, cmd_status]
     end
   end
 end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 183bd10d6a339b80de9aedea6d559c18fd7d6413..b8326a64b222603087207c17302b7463e81148b6 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -5,11 +5,7 @@ module Gitlab
     def initialize(current_user, project, query, repository_ref = nil)
       @current_user = current_user
       @project = project
-      @repository_ref = if repository_ref.present?
-                          repository_ref
-                        else
-                          nil
-                        end
+      @repository_ref = repository_ref.presence
       @query = query
     end
 
@@ -28,11 +24,6 @@ module Gitlab
       end
     end
 
-    def total_count
-      @total_count ||= issues_count + merge_requests_count + blobs_count +
-                       notes_count + wiki_blobs_count + commits_count
-    end
-
     def blobs_count
       @blobs_count ||= blobs.count
     end
@@ -52,37 +43,31 @@ module Gitlab
     private
 
     def blobs
-      if project.empty_repo? || query.blank?
-        []
-      else
-        project.repository.search_files(query, repository_ref)
-      end
+      @blobs ||= project.repository.search_files(query, repository_ref)
     end
 
     def wiki_blobs
-      if project.wiki_enabled? && query.present?
-        project_wiki = ProjectWiki.new(project)
+      @wiki_blobs ||= begin
+        if project.wiki_enabled? && query.present?
+          project_wiki = ProjectWiki.new(project)
 
-        unless project_wiki.empty?
-          project_wiki.search_files(query)
+          unless project_wiki.empty?
+            project_wiki.search_files(query)
+          else
+            []
+          end
         else
           []
         end
-      else
-        []
       end
     end
 
     def notes
-      project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+      @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
     end
 
     def commits
-      if project.empty_repo? || query.blank?
-        []
-      else
-        project.repository.find_commits_by_message(query).compact
-      end
+      @commits ||= project.repository.find_commits_by_message(query)
     end
 
     def project_ids_relation
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9376b54f43bc102cf1c5494659332adda38d515e..9226da2d6b1e03c6e0351a4c69213a7336d15e93 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -9,35 +9,45 @@ module Gitlab
     SIDEKIQ_NAMESPACE = 'resque:gitlab'
     MAILROOM_NAMESPACE = 'mail_room:gitlab'
     DEFAULT_REDIS_URL = 'redis://localhost:6379'
-
-    # To be thread-safe we must be careful when writing the class instance
-    # variables @url and @pool. Because @pool depends on @url we need two
-    # mutexes to prevent deadlock.
-    PARAMS_MUTEX = Mutex.new
-    POOL_MUTEX = Mutex.new
-    private_constant :PARAMS_MUTEX, :POOL_MUTEX
+    CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
 
     class << self
+      # Do NOT cache in an instance variable. Result may be mutated by caller.
       def params
-        @params || PARAMS_MUTEX.synchronize { @params = new.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
 
       def with
-        if @pool.nil?
-          POOL_MUTEX.synchronize do
-            @pool = ConnectionPool.new { ::Redis.new(params) }
-          end
-        end
+        @pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
         @pool.with { |redis| yield redis }
       end
 
-      def reset_params!
-        @params = nil
+      def pool_size
+        if Sidekiq.server?
+          # the pool will be used in a multi-threaded context
+          Sidekiq.options[:concurrency] + 5
+        else
+          # probably this is a Unicorn process, so single threaded
+          5
+        end
+      end
+
+      def _raw_config
+        return @_raw_config if defined?(@_raw_config)
+
+        begin
+          @_raw_config = File.read(CONFIG_FILE).freeze
+        rescue Errno::ENOENT
+          @_raw_config = false
+        end
+
+        @_raw_config
       end
     end
 
@@ -53,6 +63,14 @@ module Gitlab
       raw_config_hash[:url]
     end
 
+    def sentinels
+      raw_config_hash[:sentinels]
+    end
+
+    def sentinels?
+      sentinels && !sentinels.empty?
+    end
+
     private
 
     def redis_store_options
@@ -83,12 +101,7 @@ module Gitlab
     end
 
     def fetch_config
-      file = config_file
-      File.exist?(file) ? YAML.load_file(file)[@rails_env] : false
-    end
-
-    def config_file
-      File.expand_path('../../../config/resque.yml', __FILE__)
+      self.class._raw_config ? YAML.load(self.class._raw_config)[@rails_env] : false
     end
   end
 end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index ffad5e17c78394f621e9a4514b0b7a32d31ebef6..cb1659f9cee9cfc1b6a197aca46a8d0a44ab0d28 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,15 +2,19 @@ module Gitlab
   module Regex
     extend self
 
-    NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze
+    NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
 
     def namespace_regex
       @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
     end
 
+    def namespace_route_regex
+      @namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze
+    end
+
     def namespace_regex_message
       "can contain only letters, digits, '_', '-' and '.'. " \
-      "Cannot start with '-' or end in '.'." \
+      "Cannot start with '-' or end in '.', '.git' or '.atom'." \
     end
 
     def namespace_name_regex
@@ -44,7 +48,7 @@ module Gitlab
     end
 
     def file_name_regex_message
-      "can contain only letters, digits, '_', '-', '@' and '.'. "
+      "can contain only letters, digits, '_', '-', '@' and '.'."
     end
 
     def file_path_regex
@@ -52,7 +56,7 @@ module Gitlab
     end
 
     def file_path_regex_message
-      "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'. "
+      "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'."
     end
 
     def directory_traversal_regex
@@ -60,7 +64,7 @@ module Gitlab
     end
 
     def directory_traversal_regex_message
-      "cannot include directory traversal. "
+      "cannot include directory traversal."
     end
 
     def archive_formats_regex
@@ -96,11 +100,11 @@ module Gitlab
     end
 
     def environment_name_regex
-      @environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze
+      @environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
     end
 
     def environment_name_regex_message
-      "can contain only letters, digits, '-' and '_'."
+      "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
     end
   end
 end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f8ab2b1f09ec0acafaee12bcb8d7eb3375983c7d..2690938fe820c6cc5b442c3209b3bc497a254b3b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -27,11 +27,6 @@ module Gitlab
       end
     end
 
-    def total_count
-      @total_count ||= projects_count + issues_count + merge_requests_count +
-        milestones_count
-    end
-
     def projects_count
       @projects_count ||= projects.count
     end
@@ -48,10 +43,6 @@ module Gitlab
       @milestones_count ||= milestones.count
     end
 
-    def empty?
-      total_count.zero?
-    end
-
     private
 
     def projects
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
new file mode 100644
index 0000000000000000000000000000000000000000..117fc5081359d70250f09ba27bde84982c775057
--- /dev/null
+++ b/lib/gitlab/sentry.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module Sentry
+    def self.enabled?
+      Rails.env.production? && current_application_settings.sentry_enabled?
+    end
+
+    def self.context(current_user = nil)
+      return unless self.enabled?
+
+      if current_user
+        Raven.user_context(
+          id: current_user.id,
+          email: current_user.email,
+          username: current_user.username,
+        )
+      end
+    end
+
+    def self.program_context
+      if Sidekiq.server?
+        'sidekiq'
+      else
+        'rails'
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
index 7813091ec7b44f8b74d647d4417183819493f363..82a59a7a87e499e439d6aec81d7a594cde37f76f 100644
--- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
@@ -2,7 +2,7 @@ module Gitlab
   module SidekiqMiddleware
     class ArgumentsLogger
       def call(worker, job, queue)
-        Sidekiq.logger.info "arguments: #{job['args']}"
+        Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}"
         yield
       end
     end
diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60d35be259912e34e905c337e0ed321796e9ac2a
--- /dev/null
+++ b/lib/gitlab/slash_commands/command_definition.rb
@@ -0,0 +1,57 @@
+module Gitlab
+  module SlashCommands
+    class CommandDefinition
+      attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block
+
+      def initialize(name, attributes = {})
+        @name = name
+
+        @aliases         = attributes[:aliases] || []
+        @description     = attributes[:description] || ''
+        @params          = attributes[:params] || []
+        @condition_block = attributes[:condition_block]
+        @action_block    = attributes[:action_block]
+      end
+
+      def all_names
+        [name, *aliases]
+      end
+
+      def noop?
+        action_block.nil?
+      end
+
+      def available?(opts)
+        return true unless condition_block
+
+        context = OpenStruct.new(opts)
+        context.instance_exec(&condition_block)
+      end
+
+      def execute(context, opts, arg)
+        return if noop? || !available?(opts)
+
+        if arg.present?
+          context.instance_exec(arg, &action_block)
+        elsif action_block.arity == 0
+          context.instance_exec(&action_block)
+        end
+      end
+
+      def to_h(opts)
+        desc = description
+        if desc.respond_to?(:call)
+          context = OpenStruct.new(opts)
+          desc = context.instance_exec(&desc) rescue ''
+        end
+
+        {
+          name: name,
+          aliases: aliases,
+          description: desc,
+          params: params
+        }
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb
new file mode 100644
index 0000000000000000000000000000000000000000..50b0937d267cdf870bb24cf42871155979fb22e2
--- /dev/null
+++ b/lib/gitlab/slash_commands/dsl.rb
@@ -0,0 +1,98 @@
+module Gitlab
+  module SlashCommands
+    module Dsl
+      extend ActiveSupport::Concern
+
+      included do
+        cattr_accessor :command_definitions, instance_accessor: false do
+          []
+        end
+
+        cattr_accessor :command_definitions_by_name, instance_accessor: false do
+          {}
+        end
+      end
+
+      class_methods do
+        # Allows to give a description to the next slash command.
+        # This description is shown in the autocomplete menu.
+        # It accepts a block that will be evaluated with the context given to
+        # `CommandDefintion#to_h`.
+        #
+        # Example:
+        #
+        #   desc do
+        #     "This is a dynamic description for #{noteable.to_ability_name}"
+        #   end
+        #   command :command_key do |arguments|
+        #     # Awesome code block
+        #   end
+        def desc(text = '', &block)
+          @description = block_given? ? block : text
+        end
+
+        # Allows to define params for the next slash command.
+        # These params are shown in the autocomplete menu.
+        #
+        # Example:
+        #
+        #   params "~label ~label2"
+        #   command :command_key do |arguments|
+        #     # Awesome code block
+        #   end
+        def params(*params)
+          @params = params
+        end
+
+        # Allows to define conditions that must be met in order for the command
+        # to be returned by `.command_names` & `.command_definitions`.
+        # It accepts a block that will be evaluated with the context given to
+        # `CommandDefintion#to_h`.
+        #
+        # Example:
+        #
+        #   condition do
+        #     project.public?
+        #   end
+        #   command :command_key do |arguments|
+        #     # Awesome code block
+        #   end
+        def condition(&block)
+          @condition_block = block
+        end
+
+        # Registers a new command which is recognizeable from body of email or
+        # comment.
+        # It accepts aliases and takes a block.
+        #
+        # Example:
+        #
+        #   command :my_command, :alias_for_my_command do |arguments|
+        #     # Awesome code block
+        #   end
+        def command(*command_names, &block)
+          name, *aliases = command_names
+
+          definition = CommandDefinition.new(
+            name,
+            aliases:          aliases,
+            description:      @description,
+            params:           @params,
+            condition_block:  @condition_block,
+            action_block:     block
+          )
+
+          self.command_definitions << definition
+
+          definition.all_names.each do |name|
+            self.command_definitions_by_name[name] = definition
+          end
+
+          @description = nil
+          @params = nil
+          @condition_block = nil
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a672e5e485528a58a1d1f562f95f80b4b4fa3ea3
--- /dev/null
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -0,0 +1,122 @@
+module Gitlab
+  module SlashCommands
+    # This class takes an array of commands that should be extracted from a
+    # given text.
+    #
+    # ```
+    # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+    # ```
+    class Extractor
+      attr_reader :command_definitions
+
+      def initialize(command_definitions)
+        @command_definitions = command_definitions
+      end
+
+      # Extracts commands from content and return an array of commands.
+      # The array looks like the following:
+      # [
+      #   ['command1'],
+      #   ['command3', 'arg1 arg2'],
+      # ]
+      # The command and the arguments are stripped.
+      # The original command text is removed from the given `content`.
+      #
+      # Usage:
+      # ```
+      # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+      # msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
+      # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
+      # msg #=> "hello\nworld"
+      # ```
+      def extract_commands(content, opts = {})
+        return [content, []] unless content
+
+        content = content.dup
+
+        commands = []
+
+        content.delete!("\r")
+        content.gsub!(commands_regex(opts)) do
+          if $~[:cmd]
+            commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
+            ''
+          else
+            $~[0]
+          end
+        end
+
+        [content.strip, commands]
+      end
+
+      private
+
+      # Builds a regular expression to match known commands.
+      # First match group captures the command name and
+      # second match group captures its arguments.
+      #
+      # It looks something like:
+      #
+      #   /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
+      def commands_regex(opts)
+        names = command_names(opts).map(&:to_s)
+
+        @commands_regex ||= %r{
+            (?<code>
+              # Code blocks:
+              # ```
+              # Anything, including `/cmd arg` which are ignored by this filter
+              # ```
+
+              ^```
+              .+?
+              \n```$
+            )
+          |
+            (?<html>
+              # HTML block:
+              # <tag>
+              # Anything, including `/cmd arg` which are ignored by this filter
+              # </tag>
+
+              ^<[^>]+?>\n
+              .+?
+              \n<\/[^>]+?>$
+            )
+          |
+            (?<html>
+              # Quote block:
+              # >>>
+              # Anything, including `/cmd arg` which are ignored by this filter
+              # >>>
+
+              ^>>>
+              .+?
+              \n>>>$
+            )
+          |
+            (?:
+              # Command not in a blockquote, blockcode, or HTML tag:
+              # /close
+
+              ^\/
+              (?<cmd>#{Regexp.union(names)})
+              (?:
+                [ ]
+                (?<arg>[^\/\n]*)
+              )?
+              (?:\n|$)
+            )
+        }mx
+      end
+
+      def command_names(opts)
+        command_definitions.flat_map do |command|
+          next if command.noop?
+
+          command.all_names
+        end.compact
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index e0e74ff8359f637faf72211766d5f88cb7268a8d..9e01f02029c1a7fd0e7e9ef0fcb1fa13fb0c90e5 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -20,10 +20,6 @@ module Gitlab
       end
     end
 
-    def total_count
-      @total_count ||= snippet_titles_count + snippet_blobs_count
-    end
-
     def snippet_titles_count
       @snippet_titles_count ||= snippet_titles.count
     end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index fe65c246101e99c4aeac07f576a36f85c6491168..99d0c28e7493911e9c8789b43599a75938b34ca0 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -22,6 +22,8 @@ module Gitlab
         note_url
       when WikiPage
         wiki_page_url
+      when ProjectSnippet
+        project_snippet_url(object)
       else
         raise NotImplementedError.new("No URL builder defined for #{object.class}")
       end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9df2c8ac823caa97d0e5d363ae0f9d..4c395b4266ebb82c5e9ad99d05442830741ed451 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,11 +7,19 @@ module Gitlab
     # @param  cmd [Array<String>]
     # @return [Boolean]
     def system_silent(cmd)
-      Popen::popen(cmd).last.zero?
+      Popen.popen(cmd).last.zero?
     end
 
     def force_utf8(str)
       str.force_encoding(Encoding::UTF_8)
     end
+
+    def to_boolean(value)
+      return value if [true, false].include?(value)
+      return true if value =~ /^(true|t|yes|y|1|on)$/i
+      return false if value =~ /^(false|f|no|n|0|off)$/i
+
+      nil
+    end
   end
 end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c6826a09bd285cf5e9b543f425460a22ebf84c18..594439a5d4b3a6f979d362e58ebf39187faf651e 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -1,19 +1,38 @@
 require 'base64'
 require 'json'
+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'
+
+    # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
+    # bytes https://tools.ietf.org/html/rfc4868#section-2.6
+    SECRET_LENGTH = 32
 
     class << self
       def git_http_ok(repository, user)
         {
-          'GL_ID' => Gitlab::GlId.gl_id(user),
-          'RepoPath' => repository.path_to_repo,
+          GL_ID: Gitlab::GlId.gl_id(user),
+          RepoPath: repository.path_to_repo,
+        }
+      end
+
+      def lfs_upload_ok(oid, size)
+        {
+          StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+          LfsOid: oid,
+          LfsSize: size,
         }
       end
 
+      def artifact_upload_ok
+        { TempPath: ArtifactUploader.artifacts_upload_path }
+      end
+
       def send_git_blob(repository, blob)
         params = {
           'RepoPath' => repository.path_to_repo,
@@ -41,7 +60,7 @@ module Gitlab
       def send_git_diff(repository, diff_refs)
         params = {
           'RepoPath'  => repository.path_to_repo,
-          'ShaFrom'   => diff_refs.start_sha,
+          'ShaFrom'   => diff_refs.base_sha,
           'ShaTo'     => diff_refs.head_sha
         }
 
@@ -54,7 +73,7 @@ module Gitlab
       def send_git_patch(repository, diff_refs)
         params = {
           'RepoPath'  => repository.path_to_repo,
-          'ShaFrom'   => diff_refs.start_sha,
+          'ShaFrom'   => diff_refs.base_sha,
           'ShaTo'     => diff_refs.head_sha
         }
 
@@ -81,6 +100,35 @@ module Gitlab
         path.readable? ? path.read.chomp : 'unknown'
       end
 
+      def secret
+        @secret ||= begin
+          bytes = Base64.strict_decode64(File.read(secret_path).chomp)
+          raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
+          bytes
+        end
+      end
+
+      def write_secret
+        bytes = SecureRandom.random_bytes(SECRET_LENGTH)
+        File.open(secret_path, 'w:BINARY', 0600) do |f|
+          f.chmod(0600) # If the file already existed, the '0600' passed to 'open' above was a no-op.
+          f.write(Base64.strict_encode64(bytes))
+        end
+      end
+
+      def verify_api_request!(request_headers)
+        JWT.decode(
+          request_headers[INTERNAL_API_REQUEST_HEADER],
+          secret,
+          true,
+          { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
+        )
+      end
+
+      def secret_path
+        Rails.root.join('.gitlab_workhorse_secret')
+      end
+
       protected
 
       def encode(hash)
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 2214f855200e4c8984ab3cb65b2cb549d9ad1833..78ae187817aa5c80b842dfdcc21f4314c084c182 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,22 +1,33 @@
 namespace :cache do
-  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
+  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
 
-  desc "GitLab | Clear redis cache"
-  task :clear => :environment do
-    Gitlab::Redis.with do |redis|
-      cursor = REDIS_SCAN_START_STOP
-      loop do
-        cursor, keys = redis.scan(
-          cursor,
-          match: "#{Gitlab::Redis::CACHE_NAMESPACE}*", 
-          count: CLEAR_BATCH_SIZE
-        )
-  
-        redis.del(*keys) if keys.any?
-  
-        break if cursor == REDIS_SCAN_START_STOP
+    desc "GitLab | Clear redis cache"
+    task redis: :environment do
+      Gitlab::Redis.with do |redis|
+        cursor = REDIS_SCAN_START_STOP
+        loop do
+          cursor, keys = redis.scan(
+            cursor,
+            match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
+            count: REDIS_CLEAR_BATCH_SIZE
+          )
+
+          redis.del(*keys) if keys.any?
+
+          break if cursor == REDIS_SCAN_START_STOP
+        end
       end
     end
+
+    desc "GitLab | Clear database cache (in the background)"
+    task db: :environment do
+      ClearDatabaseCacheWorker.perform_async
+    end
+
+    task all: [:db, :redis]
   end
+
+  task clear: 'cache:clear:redis'
 end
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/lib/tasks/ee_compat_check.rake b/lib/tasks/ee_compat_check.rake
new file mode 100644
index 0000000000000000000000000000000000000000..f494fa5c5c28967d20116245a715c1aef2414dc1
--- /dev/null
+++ b/lib/tasks/ee_compat_check.rake
@@ -0,0 +1,4 @@
+desc 'Checks if the branch would apply cleanly to EE'
+task ee_compat_check: :environment do
+  Rake::Task['gitlab:dev:ee_compat_check'].invoke
+end
diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake
new file mode 100644
index 0000000000000000000000000000000000000000..d43cbad1909a527923d071253f560ce10173c40c
--- /dev/null
+++ b/lib/tasks/eslint.rake
@@ -0,0 +1,7 @@
+unless Rails.env.production?
+  desc "GitLab | Run ESLint"
+  task :eslint do
+    system("npm", "run", "eslint")
+  end
+end
+
diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake
deleted file mode 100644
index 3bfe999ae74d4020c64ebe7b36515d561ff3a178..0000000000000000000000000000000000000000
--- a/lib/tasks/flog.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-desc 'Code complexity analyze via flog'
-task :flog do
-  output = %x(bundle exec flog -m app/ lib/gitlab)
-  exit_code = 0
-  minimum_score = 70
-  output = output.lines
-
-  # Skip total complexity score
-  output.shift
-
-  # Skip some trash info
-  output.shift
-
-  output.each do |line|
-    score, method = line.split(" ")
-    score = score.to_i
-
-    if score > minimum_score
-      exit_code = 1
-      puts "High complexity in #{method}. Score: #{score}"
-    end
-  end
-
-  exit exit_code
-end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index b43ee5b338312ee2e13bc824ce0f23bbe6c1092d..a9f1255e8cf3190bdc6989df28248cc988358f30 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -51,6 +51,7 @@ namespace :gitlab do
         $progress.puts 'done'.color(:green)
         Rake::Task['gitlab:backup:db:restore'].invoke
       end
+
       Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
       Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
       Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
@@ -58,6 +59,7 @@ namespace :gitlab do
       Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
       Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
       Rake::Task['gitlab:shell:setup'].invoke
+      Rake::Task['cache:clear'].invoke
 
       backup.cleanup
     end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 5f4a6bbfa353271840c7c3f60beae2bcff47b75c..35c4194e87c281a9fc667694f27292d1cf8f6a0c 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -671,7 +671,7 @@ namespace :gitlab do
           "Enable mail_room in the init.d configuration."
         )
         for_more_information(
-          "doc/incoming_email/README.md"
+          "doc/administration/reply_by_email.md"
         )
         fix_and_rerun
       end
@@ -690,7 +690,7 @@ namespace :gitlab do
           "Enable mail_room in your Procfile."
         )
         for_more_information(
-          "doc/incoming_email/README.md"
+          "doc/administration/reply_by_email.md"
         )
         fix_and_rerun
       end
@@ -747,7 +747,7 @@ namespace :gitlab do
           "Check that the information in config/gitlab.yml is correct"
         )
         for_more_information(
-          "doc/incoming_email/README.md"
+          "doc/administration/reply_by_email.md"
         )
         fix_and_rerun
       end
@@ -760,7 +760,7 @@ namespace :gitlab do
   end
 
   namespace :ldap do
-    task :check, [:limit] => :environment do |t, args|
+    task :check, [:limit] => :environment do |_, args|
       # Only show up to 100 results because LDAP directories can be very big.
       # This setting only affects the `rake gitlab:check` script.
       args.with_defaults(limit: 100)
@@ -768,7 +768,7 @@ namespace :gitlab do
       start_checking "LDAP"
 
       if Gitlab::LDAP::Config.enabled?
-        print_users(args.limit)
+        check_ldap(args.limit)
       else
         puts 'LDAP is disabled in config/gitlab.yml'
       end
@@ -776,21 +776,42 @@ namespace :gitlab do
       finished_checking "LDAP"
     end
 
-    def print_users(limit)
-      puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
-
+    def check_ldap(limit)
       servers = Gitlab::LDAP::Config.providers
 
       servers.each do |server|
         puts "Server: #{server}"
-        Gitlab::LDAP::Adapter.open(server) do |adapter|
-          users = adapter.users(adapter.config.uid, '*', limit)
-          users.each do |user|
-            puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+
+        begin
+          Gitlab::LDAP::Adapter.open(server) do |adapter|
+            check_ldap_auth(adapter)
+
+            puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
+
+            users = adapter.users(adapter.config.uid, '*', limit)
+            users.each do |user|
+              puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+            end
           end
+        rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
+          puts "Could not connect to the LDAP server: #{e.message}".color(:red)
         end
       end
     end
+
+    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
+
+      puts "LDAP authentication... #{message}"
+    end
   end
 
   namespace :repo do
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
new file mode 100644
index 0000000000000000000000000000000000000000..3117075b08b06ec7a0a38805cc944d23c3a5fd58
--- /dev/null
+++ b/lib/tasks/gitlab/dev.rake
@@ -0,0 +1,26 @@
+namespace :gitlab do
+  namespace :dev do
+    desc 'Checks if the branch would apply cleanly to EE'
+    task :ee_compat_check, [:branch] => :environment do |_, args|
+      opts =
+        if ENV['CI']
+          {
+            branch: ENV['CI_BUILD_REF_NAME'],
+            ce_repo: ENV['CI_BUILD_REPO']
+          }
+        else
+          unless args[:branch]
+            puts "Must specify a branch as an argument".color(:red)
+            exit 1
+          end
+          args
+        end
+
+      if Gitlab::EeCompatCheck.new(opts || {}).check
+        exit 0
+      else
+        exit 1
+      end
+    end
+  end
+end
diff --git a/lib/tasks/gitlab/generate_docs.rake b/lib/tasks/gitlab/generate_docs.rake
deleted file mode 100644
index f6448c38e1006b2d7c3c320070d579e68ee59259..0000000000000000000000000000000000000000
--- a/lib/tasks/gitlab/generate_docs.rake
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace :gitlab do
-  desc "GitLab | Generate sdocs for project"
-  task generate_docs: :environment do
-    system(*%W(bundle exec sdoc -o doc/code app lib))
-  end
-end
-
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index bb7eb852f1b8603e7a6aced3ce550edebf86e2ea..58761a129d42a41fb078d75990413bdee08e1505 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -63,11 +63,11 @@ namespace :gitlab do
 
         # Launch installation process
         system(*%W(bin/install) + repository_storage_paths_args)
-
-        # (Re)create hooks
-        system(*%W(bin/create-hooks) + repository_storage_paths_args)
       end
 
+      # (Re)create hooks
+      Rake::Task['gitlab:shell:create_hooks'].invoke
+
       # Required for debian packaging with PKGR: Setup .ssh/environment with
       # the current PATH, so that the correct ruby version gets loaded
       # Requires to set "PermitUserEnvironment yes" in sshd config (should not
@@ -78,7 +78,7 @@ namespace :gitlab do
         f.puts "PATH=#{ENV['PATH']}"
       end
 
-      Gitlab::Shell.new.generate_and_link_secret_token
+      Gitlab::Shell.ensure_secret_token!
     end
 
     desc "GitLab | Setup gitlab-shell"
@@ -102,6 +102,15 @@ namespace :gitlab do
         end
       end
     end
+
+    desc 'Create or repair repository hooks symlink'
+    task create_hooks: :environment do
+      warn_user_is_not_gitlab
+
+      puts 'Creating/Repairing hooks symlinks for all repositories'
+      system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
+      puts 'done'.color(:green)
+    end
   end
 
   def setup
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
new file mode 100644
index 0000000000000000000000000000000000000000..3a16ace60bdd6e2414f9fa7484c2f60fe950f25f
--- /dev/null
+++ b/lib/tasks/gitlab/users.rake
@@ -0,0 +1,11 @@
+namespace :gitlab do
+  namespace :users do
+    desc "GitLab | Clear the authentication token for all users"
+    task clear_all_authentication_tokens: :environment  do |t, args|
+      # Do small batched updates because these updates will be slow and locking
+      User.select(:id).find_in_batches(batch_size: 100) do |batch|
+        User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
+      end
+    end
+  end
+end
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
new file mode 100644
index 0000000000000000000000000000000000000000..609dfaa48e3f33eacd36e54e86a0f548c8df23c2
--- /dev/null
+++ b/lib/tasks/haml-lint.rake
@@ -0,0 +1,5 @@
+unless Rails.env.production?
+  require 'haml_lint/rake_task'
+
+  HamlLint::RakeTask.new
+end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
new file mode 100644
index 0000000000000000000000000000000000000000..32b668df3bf1a85ea0a2ded4938805c394f89431
--- /dev/null
+++ b/lib/tasks/lint.rake
@@ -0,0 +1,9 @@
+unless Rails.env.production?
+  namespace :lint do
+    desc "GitLab | lint | Lint JavaScript files using ESLint"
+    task :javascript do
+      Rake::Task['eslint'].invoke
+    end
+  end
+end
+
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
new file mode 100644
index 0000000000000000000000000000000000000000..08caedd7ff32d09ea9d2cc47f70ced7f91cbe654
--- /dev/null
+++ b/lib/tasks/teaspoon.rake
@@ -0,0 +1,25 @@
+unless Rails.env.production?
+  Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
+
+  namespace :teaspoon do
+    desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
+    RSpec::Core::RakeTask.new(:fixtures) do |t|
+      ENV['NO_KNAPSACK'] = 'true'
+      t.pattern = 'spec/javascripts/fixtures/*.rb'
+      t.rspec_opts = '--format documentation'
+    end
+
+    desc 'GitLab | Teaspoon | Run JavaScript tests'
+    task :tests do
+      require "teaspoon/console"
+      options = {}
+      abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
+    end
+  end
+
+  desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
+  task :teaspoon do
+    Rake::Task['teaspoon:fixtures'].invoke
+    Rake::Task['teaspoon:tests'].invoke
+  end
+end
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e75e070451b4f31b8efde868712081d73626f94a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+  "private": true,
+  "scripts": {
+    "eslint": "eslint --ext .js,.js.es6 .",
+    "eslint-fix": "eslint --fix --ext .js,.js.es6 ."
+  },
+  "devDependencies": {
+    "eslint": "^3.1.1",
+    "eslint-config-airbnb": "^12.0.0",
+    "eslint-plugin-filenames": "^1.1.0",
+    "eslint-plugin-import": "^2.0.1",
+    "eslint-plugin-jasmine": "^1.8.1",
+    "eslint-plugin-jsx-a11y": "^2.2.3",
+    "eslint-plugin-react": "^6.4.1"
+  }
+}
diff --git a/public/deploy.html b/public/deploy.html
index 142472b6c35f716812f294376126939c614f26ab..49ec4ac5ce18b9171abb3cd8406305099cb58512 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -2,6 +2,11 @@
 <html>
 <head>
   <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
+  <meta name="refresh" content="60">
+  <meta name="retry-after" content="100">
+  <meta name="robots" content="noindex, nofollow, noarchive, nostore">
+  <meta name="cache-control" content="no-cache, no-store">
+  <meta name="pragma" content="no-cache">
   <title>Deploy in progress</title>
   <style>
     body {
@@ -61,4 +66,4 @@
     <p>Please contact your GitLab administrator if this problem persists.</p>
   </div>
 </body>
-</html>
+</html>
\ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
index 334f4c035334f74dbb8dca25eddc0d95619fed64..7d69fad59d1a9cc22714d5a565eb592c26da0d39 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -23,7 +23,7 @@ Disallow: /groups/*/edit
 Disallow: /users
 
 # Global snippets
-Disallow: /s
+Disallow: /s/
 Disallow: /snippets/new
 Disallow: /snippets/*/edit
 Disallow: /snippets/*/raw
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc6e4d940611423a83dc9b664099dc11b4c1fa99..7c4e82769029dcf09b7c4a642d6c8d0822f780c1 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -10,6 +10,15 @@ then
   exit 1
 fi
 
+# Ensure that the CHANGELOG.md does not contain duplicate versions
+DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
+if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
+then
+  echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
+  echo "${DUPLICATE_CHANGELOG_VERSIONS}" >&2
+  exit 1
+fi
+
 echo "✔ Linting passed"
 exit 0
 
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 76b2178c79c346281e03582b558bdcc3c535976c..1eaafdce389866943317e01c6260b8b2250d7b58 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -16,21 +16,6 @@ retry() {
 }
 
 if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
-    mkdir -p vendor/apt
-
-    # Install phantomjs package
-    pushd vendor/apt
-    PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64"
-    if [ ! -d "$PHANTOMJS_FILE" ]; then
-        curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx
-    fi
-    cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
-    popd
-
-    # Try to install packages
-    retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
-      libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
-
     cp config/database.yml.mysql config/database.yml
     sed -i 's/username:.*/username: root/g' config/database.yml
     sed -i 's/password:.*/password:/g' config/database.yml
@@ -39,7 +24,7 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
     cp config/resque.yml.example config/resque.yml
     sed -i 's/localhost/redis/g' config/resque.yml
 
-    export FLAGS=(--path vendor --retry 3)
+    export FLAGS=(--path vendor --retry 3 --quiet)
 else
     export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
     cp config/database.yml.mysql config/database.yml
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f4298db59f77d51c943fc01340911b023a72d98
--- /dev/null
+++ b/spec/bin/changelog_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+load File.expand_path('../../bin/changelog', __dir__)
+
+describe 'bin/changelog' do
+  describe ChangelogOptionParser do
+    it 'parses --ammend' do
+      options = described_class.parse(%w[foo bar --amend])
+
+      expect(options.amend).to eq true
+    end
+
+    it 'parses --force and -f' do
+      %w[--force -f].each do |flag|
+        options = described_class.parse(%W[foo #{flag} bar])
+
+        expect(options.force).to eq true
+      end
+    end
+
+    it 'parses --merge-request and -m' do
+      %w[--merge-request -m].each do |flag|
+        options = described_class.parse(%W[foo #{flag} 1234 bar])
+
+        expect(options.merge_request).to eq 1234
+      end
+    end
+
+    it 'parses --dry-run and -n' do
+      %w[--dry-run -n].each do |flag|
+        options = described_class.parse(%W[foo #{flag} bar])
+
+        expect(options.dry_run).to eq true
+      end
+    end
+
+    it 'parses --git-username and -u' do
+      allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
+
+      %w[--git-username -u].each do |flag|
+        options = described_class.parse(%W[foo #{flag} bar])
+
+        expect(options.author).to eq 'Jane Doe'
+      end
+    end
+
+    it 'parses -h' do
+      expect do
+        $stdout = StringIO.new
+
+        described_class.parse(%w[foo -h bar])
+      end.to raise_error(SystemExit)
+    end
+
+    it 'assigns title' do
+      options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+
+      expect(options.title).to eq 'foo bar baz'
+    end
+  end
+end
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index c5d3cd70acc87deddf951ec56f26d9cb9cd2f7c8..22bf3055538a92e6bd7a574d1cabe6f03751357e 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -3,6 +3,8 @@ 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 }
 
   context 'when incoming email is disabled' do
     before do
@@ -20,6 +22,9 @@ 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/mail_room_enabled.yml').to_s
       Gitlab::MailRoom.reset_config!
@@ -30,8 +35,9 @@ describe 'mail_room.yml' do
     end
 
     it 'contains the intended configuration' do
-      expect(configuration[:mailboxes].length).to eq(1)
+      stub_const('Gitlab::Redis::CONFIG_FILE', redis_config)
 
+      expect(configuration[:mailboxes].length).to eq(1)
       mailbox = configuration[:mailboxes].first
 
       expect(mailbox[:host]).to eq('imap.gmail.com')
@@ -42,10 +48,26 @@ describe 'mail_room.yml' do
       expect(mailbox[:password]).to eq('[REDACTED]')
       expect(mailbox[:name]).to eq('inbox')
 
-      redis_url = Gitlab::Redis.url
+      redis_url = gitlab_redis.url
+      sentinels = gitlab_redis.sentinels
 
+      expect(mailbox[:delivery_options][:redis_url]).to be_present
       expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
+
+      expect(mailbox[:delivery_options][:sentinels]).to be_present
+      expect(mailbox[:delivery_options][:sentinels]).to eq(sentinels)
+
+      expect(mailbox[:arbitration_options][:redis_url]).to be_present
       expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
+
+      expect(mailbox[:arbitration_options][:sentinels]).to be_present
+      expect(mailbox[:arbitration_options][:sentinels]).to eq(sentinels)
     end
   end
+
+  def clear_raw_config
+    Gitlab::Redis.remove_instance_variable(:@_raw_config)
+  rescue NameError
+    # raised if @_raw_config was not set; ignore
+  end
 end
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index d5f0b289b5b64993b7b4bec4f58bacfc5e48d015..8be662974a0702b9ef244320d5da582a5a3ad653 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -77,6 +77,8 @@ describe Admin::ImpersonationsController do
 
           context "when the impersonator is not blocked" do
             it "redirects to the impersonated user's page" do
+              expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original
+
               delete :destroy
 
               expect(response).to redirect_to(admin_user_path(user))
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 33fe3c73822673c9503595d48124e88036647afc..2ab2ca1b66768e2f1dd716fed76e889e0099c818 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -114,6 +114,17 @@ describe Admin::UsersController do
     end
   end
 
+  describe 'POST create' do
+    it 'creates the user' do
+      expect{ post :create, user: attributes_for(:user) }.to change{ User.count }.by(1)
+    end
+
+    it 'shows only one error message for an invalid email' do
+      post :create, user: attributes_for(:user, email: 'bogus')
+      expect(assigns[:user].errors).to contain_exactly("Email is invalid")
+    end
+  end
+
   describe 'POST update' do
     context 'when the password has changed' do
       def update_password(user, password, password_confirmation = nil)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 44128a4336234d99fd98bf6a835b8eb747f62f54..a121cb2fc97d3e58960e660073c595ab9fe1f741 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -237,6 +237,56 @@ describe AutocompleteController do
       end
     end
 
+    context 'authorized projects apply limit' do
+      before do
+        authorized_project2 = create(:project)
+        authorized_project3 = create(:project)
+
+        authorized_project.team << [user, :master]
+        authorized_project2.team << [user, :master]
+        authorized_project3.team << [user, :master]
+
+        stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+      end
+
+      describe 'GET #projects with project ID' do
+        before do
+          get(:projects, project_id: project.id)
+        end
+
+        let(:body) { JSON.parse(response.body) }
+
+        it do
+          expect(body).to be_kind_of(Array)
+          expect(body.size).to eq 3 # Of a total of 4
+        end
+      end
+    end
+
+    context 'authorized projects with offset' do
+      before do
+        authorized_project2 = create(:project)
+        authorized_project3 = create(:project)
+
+        authorized_project.team << [user, :master]
+        authorized_project2.team << [user, :master]
+        authorized_project3.team << [user, :master]
+      end
+
+      describe 'GET #projects with project ID and offset_id' do
+        before do
+          get(:projects, project_id: project.id, offset_id: authorized_project.id)
+        end
+
+        let(:body) { JSON.parse(response.body) }
+
+        it do
+          expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
+          expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+        end
+      end
+    end
+
     context 'authorized projects without admin_issue ability' do
       before(:each) do
         authorized_project.team << [user, :guest]
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c34475976c6367addda843969dbf21edb8086c79..c7db84dd5f91faa8149401556af8076c0c2560fa 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,15 +2,10 @@ require 'spec_helper'
 
 describe Groups::GroupMembersController do
   let(:user)  { create(:user) }
-  let(:group) { create(:group) }
+  let(:group) { create(:group, :public) }
 
-  describe '#index' do
-    before do
-      group.add_owner(user)
-      stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
-    end
-
-    it 'renders index with group members' do
+  describe 'GET index' do
+    it 'renders index with 200 status code' do
       get :index, group_id: group
 
       expect(response).to have_http_status(200)
@@ -18,87 +13,109 @@ describe Groups::GroupMembersController do
     end
   end
 
-  describe '#destroy' do
-    let(:group) { create(:group, :public) }
+  describe 'POST create' do
+    let(:group_user) { create(:user) }
+
+    before { sign_in(user) }
+
+    context 'when user does not have enough rights' do
+      before { group.add_developer(user) }
 
-    context 'when member is not found' do
       it 'returns 403' do
-        delete :destroy, group_id: group,
-                         id: 42
+        post :create, group_id: group,
+                      user_ids: group_user.id,
+                      access_level: Gitlab::Access::GUEST
 
         expect(response).to have_http_status(403)
+        expect(group.users).not_to include group_user
       end
     end
 
-    context 'when member is found' do
-      let(:user) { create(:user) }
-      let(:group_user) { create(:user) }
-      let(:member) do
-        group.add_developer(group_user)
-        group.members.find_by(user_id: group_user)
+    context 'when user has enough rights' do
+      before { group.add_owner(user) }
+
+      it 'adds user to members' do
+        post :create, group_id: group,
+                      user_ids: group_user.id,
+                      access_level: Gitlab::Access::GUEST
+
+        expect(response).to set_flash.to 'Users were successfully added.'
+        expect(response).to redirect_to(group_group_members_path(group))
+        expect(group.users).to include group_user
+      end
+
+      it 'adds no user to members' do
+        post :create, group_id: group,
+                      user_ids: '',
+                      access_level: Gitlab::Access::GUEST
+
+        expect(response).to set_flash.to 'No users specified.'
+        expect(response).to redirect_to(group_group_members_path(group))
+        expect(group.users).not_to include group_user
+      end
+    end
+  end
+
+  describe 'DELETE destroy' do
+    let(:member) { create(:group_member, :developer, group: group) }
+
+    before { sign_in(user) }
+
+    context 'when member is not found' do
+      it 'returns 403' do
+        delete :destroy, group_id: group, id: 42
+
+        expect(response).to have_http_status(403)
       end
+    end
 
+    context 'when member is found' do
       context 'when user does not have enough rights' do
-        before do
-          group.add_developer(user)
-          sign_in(user)
-        end
+        before { group.add_developer(user) }
 
         it 'returns 403' do
-          delete :destroy, group_id: group,
-                           id: member
+          delete :destroy, group_id: group, id: member
 
           expect(response).to have_http_status(403)
-          expect(group.users).to include group_user
+          expect(group.members).to include member
         end
       end
 
       context 'when user has enough rights' do
-        before do
-          group.add_owner(user)
-          sign_in(user)
-        end
+        before { group.add_owner(user) }
 
         it '[HTML] removes user from members' do
-          delete :destroy, group_id: group,
-                           id: member
+          delete :destroy, group_id: group, id: member
 
           expect(response).to set_flash.to 'User was successfully removed from group.'
           expect(response).to redirect_to(group_group_members_path(group))
-          expect(group.users).not_to include group_user
+          expect(group.members).not_to include member
         end
 
         it '[JS] removes user from members' do
-          xhr :delete, :destroy, group_id: group,
-                                 id: member
+          xhr :delete, :destroy, group_id: group, id: member
 
           expect(response).to be_success
-          expect(group.users).not_to include group_user
+          expect(group.members).not_to include member
         end
       end
     end
   end
 
-  describe '#leave' do
-    let(:group) { create(:group, :public) }
-    let(:user) { create(:user) }
+  describe 'DELETE leave' do
+    before { sign_in(user) }
 
     context 'when member is not found' do
-      before { sign_in(user) }
-
-      it 'returns 403' do
+      it 'returns 404' do
         delete :leave, group_id: group
 
-        expect(response).to have_http_status(403)
+        expect(response).to have_http_status(404)
       end
     end
 
     context 'when member is found' do
       context 'and is not an owner' do
-        before do
-          group.add_developer(user)
-          sign_in(user)
-        end
+        before { group.add_developer(user) }
 
         it 'removes user from members' do
           delete :leave, group_id: group
@@ -110,10 +127,7 @@ describe Groups::GroupMembersController do
       end
 
       context 'and is an owner' do
-        before do
-          group.add_owner(user)
-          sign_in(user)
-        end
+        before { group.add_owner(user) }
 
         it 'cannot removes himself from the group' do
           delete :leave, group_id: group
@@ -123,10 +137,7 @@ describe Groups::GroupMembersController do
       end
 
       context 'and is a requester' do
-        before do
-          group.request_access(user)
-          sign_in(user)
-        end
+        before { group.request_access(user) }
 
         it 'removes user from members' do
           delete :leave, group_id: group
@@ -140,13 +151,8 @@ describe Groups::GroupMembersController do
     end
   end
 
-  describe '#request_access' do
-    let(:group) { create(:group, :public) }
-    let(:user) { create(:user) }
-
-    before do
-      sign_in(user)
-    end
+  describe 'POST request_access' do
+    before { sign_in(user) }
 
     it 'creates a new GroupMember that is not a team member' do
       post :request_access, group_id: group
@@ -158,53 +164,39 @@ describe Groups::GroupMembersController do
     end
   end
 
-  describe '#approve_access_request' do
-    let(:group) { create(:group, :public) }
+  describe 'POST approve_access_request' do
+    let(:member) { create(:group_member, :access_request, group: group) }
+
+    before { sign_in(user) }
 
     context 'when member is not found' do
       it 'returns 403' do
-        post :approve_access_request, group_id: group,
-                                      id: 42
+        post :approve_access_request, group_id: group, id: 42
 
         expect(response).to have_http_status(403)
       end
     end
 
     context 'when member is found' do
-      let(:user) { create(:user) }
-      let(:group_requester) { create(:user) }
-      let(:member) do
-        group.request_access(group_requester)
-        group.requesters.find_by(user_id: group_requester)
-      end
-
       context 'when user does not have enough rights' do
-        before do
-          group.add_developer(user)
-          sign_in(user)
-        end
+        before { group.add_developer(user) }
 
         it 'returns 403' do
-          post :approve_access_request, group_id: group,
-                                        id: member
+          post :approve_access_request, group_id: group, id: member
 
           expect(response).to have_http_status(403)
-          expect(group.users).not_to include group_requester
+          expect(group.members).not_to include member
         end
       end
 
       context 'when user has enough rights' do
-        before do
-          group.add_owner(user)
-          sign_in(user)
-        end
+        before { group.add_owner(user) }
 
         it 'adds user to members' do
-          post :approve_access_request, group_id: group,
-                                        id: member
+          post :approve_access_request, group_id: group, id: member
 
           expect(response).to redirect_to(group_group_members_path(group))
-          expect(group.users).to include group_requester
+          expect(group.members).to include member
         end
       end
     end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 07bf8d2d1c39c6bcc69c05fbcaa1abe83dc4afe5..1d3c9fbbe2f49d5a0d109ae616f2838478c5317e 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -146,21 +146,42 @@ describe Import::BitbucketController do
       end
 
       context "when a namespace with the Bitbucket user's username doesn't exist" do
-        it "creates the namespace" do
-          expect(Gitlab::BitbucketImport::ProjectCreator).
-            to receive(:new).and_return(double(execute: true))
+        context "when current user can create namespaces" do
+          it "creates the namespace" do
+            expect(Gitlab::BitbucketImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
 
-          post :create, format: :js
+            expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+          end
+
+          it "takes the new namespace" do
+            expect(Gitlab::BitbucketImport::ProjectCreator).
+              to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+              and_return(double(execute: true))
 
-          expect(Namespace.where(name: other_username).first).not_to be_nil
+            post :create, format: :js
+          end
         end
 
-        it "takes the new namespace" do
-          expect(Gitlab::BitbucketImport::ProjectCreator).
-            to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
-            and_return(double(execute: true))
+        context "when current user can't create namespaces" do
+          before do
+            user.update_attribute(:can_create_group, false)
+          end
 
-          post :create, format: :js
+          it "doesn't create the namespace" do
+            expect(Gitlab::BitbucketImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
+
+            expect { post :create, format: :js }.not_to change(Namespace, :count)
+          end
+
+          it "takes the current user's namespace" do
+            expect(Gitlab::BitbucketImport::ProjectCreator).
+              to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+              and_return(double(execute: true))
+
+            post :create, format: :js
+          end
         end
       end
     end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 51d595268541584acd3be7bb0164ef68788c28c1..4f96567192dccf2a60a2e8ef5465111b189a79ca 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -124,8 +124,8 @@ describe Import::GithubController do
       context "when the GitHub user and GitLab user's usernames match" do
         it "takes the current user's namespace" do
           expect(Gitlab::GithubImport::ProjectCreator).
-            to receive(:new).with(github_repo, user.namespace, user, access_params).
-            and_return(double(execute: true))
+            to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+              and_return(double(execute: true))
 
           post :create, format: :js
         end
@@ -136,8 +136,8 @@ describe Import::GithubController do
 
         it "takes the current user's namespace" do
           expect(Gitlab::GithubImport::ProjectCreator).
-            to receive(:new).with(github_repo, user.namespace, user, access_params).
-            and_return(double(execute: true))
+            to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+              and_return(double(execute: true))
 
           post :create, format: :js
         end
@@ -158,8 +158,8 @@ describe Import::GithubController do
         context "when the namespace is owned by the GitLab user" do
           it "takes the existing namespace" do
             expect(Gitlab::GithubImport::ProjectCreator).
-              to receive(:new).with(github_repo, existing_namespace, user, access_params).
-              and_return(double(execute: true))
+              to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params).
+                and_return(double(execute: true))
 
             post :create, format: :js
           end
@@ -171,9 +171,10 @@ describe Import::GithubController do
             existing_namespace.save
           end
 
-          it "doesn't create a project" do
+          it "creates a project using user's namespace" do
             expect(Gitlab::GithubImport::ProjectCreator).
-              not_to receive(:new)
+              to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+                and_return(double(execute: true))
 
             post :create, format: :js
           end
@@ -181,21 +182,63 @@ describe Import::GithubController do
       end
 
       context "when a namespace with the GitHub user's username doesn't exist" do
-        it "creates the namespace" do
-          expect(Gitlab::GithubImport::ProjectCreator).
-            to receive(:new).and_return(double(execute: true))
+        context "when current user can create namespaces" do
+          it "creates the namespace" do
+            expect(Gitlab::GithubImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
 
-          post :create, format: :js
+            expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1)
+          end
+
+          it "takes the new namespace" do
+            expect(Gitlab::GithubImport::ProjectCreator).
+              to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params).
+              and_return(double(execute: true))
+
+            post :create, target_namespace: github_repo.name, format: :js
+          end
+        end
+
+        context "when current user can't create namespaces" do
+          before do
+            user.update_attribute(:can_create_group, false)
+          end
+
+          it "doesn't create the namespace" do
+            expect(Gitlab::GithubImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
+
+            expect { post :create, format: :js }.not_to change(Namespace, :count)
+          end
 
-          expect(Namespace.where(name: other_username).first).not_to be_nil
+          it "takes the current user's namespace" do
+            expect(Gitlab::GithubImport::ProjectCreator).
+              to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+              and_return(double(execute: true))
+
+            post :create, format: :js
+          end
         end
+      end
 
-        it "takes the new namespace" do
+      context 'user has chosen a namespace and name for the project' do
+        let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+        let(:test_name) { 'test_name' }
+
+        it 'takes the selected namespace and name' do
           expect(Gitlab::GithubImport::ProjectCreator).
-            to receive(:new).with(github_repo, an_instance_of(Group), user, access_params).
-            and_return(double(execute: true))
+            to receive(:new).with(github_repo, test_name, test_namespace, user, access_params).
+              and_return(double(execute: true))
 
-          post :create, format: :js
+          post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
+        end
+
+        it 'takes the selected name and default namespace' do
+          expect(Gitlab::GithubImport::ProjectCreator).
+            to receive(:new).with(github_repo, test_name, user.namespace, user, access_params).
+              and_return(double(execute: true))
+
+          post :create, { new_name: test_name, format: :js }
         end
       end
     end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e8cf6aa7767210577adc7d9128478cbfddf32010..6f75ebb16c818f61ff5f80d45123fe55ac430b8e 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -136,21 +136,42 @@ describe Import::GitlabController do
       end
 
       context "when a namespace with the GitLab.com user's username doesn't exist" do
-        it "creates the namespace" do
-          expect(Gitlab::GitlabImport::ProjectCreator).
-            to receive(:new).and_return(double(execute: true))
+        context "when current user can create namespaces" do
+          it "creates the namespace" do
+            expect(Gitlab::GitlabImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
 
-          post :create, format: :js
+            expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+          end
+
+          it "takes the new namespace" do
+            expect(Gitlab::GitlabImport::ProjectCreator).
+              to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
+              and_return(double(execute: true))
 
-          expect(Namespace.where(name: other_username).first).not_to be_nil
+            post :create, format: :js
+          end
         end
 
-        it "takes the new namespace" do
-          expect(Gitlab::GitlabImport::ProjectCreator).
-            to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
-            and_return(double(execute: true))
+        context "when current user can't create namespaces" do
+          before do
+            user.update_attribute(:can_create_group, false)
+          end
 
-          post :create, format: :js
+          it "doesn't create the namespace" do
+            expect(Gitlab::GitlabImport::ProjectCreator).
+              to receive(:new).and_return(double(execute: true))
+
+            expect { post :create, format: :js }.not_to change(Namespace, :count)
+          end
+
+          it "takes the current user's namespace" do
+            expect(Gitlab::GitlabImport::ProjectCreator).
+              to receive(:new).with(gitlab_repo, user.namespace, user, access_params).
+              and_return(double(execute: true))
+
+            post :create, format: :js
+          end
         end
       end
     end
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
deleted file mode 100644
index 4ae2b78e11cb1e6befedba1f0b908dd49b835f29..0000000000000000000000000000000000000000
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Import::GitoriousController do
-  include ImportSpecHelper
-
-  let(:user) { create(:user) }
-
-  before do
-    sign_in(user)
-  end
-
-  describe "GET new" do
-    it "redirects to import endpoint on gitorious.org" do
-      get :new
-
-      expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback")
-    end
-  end
-
-  describe "GET callback" do
-    it "stores repo list in session" do
-      get :callback, repos: 'foo/bar,baz/qux'
-
-      expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux')
-    end
-  end
-
-  describe "GET status" do
-    before do
-      @repo = OpenStruct.new(full_name: 'asd/vim')
-    end
-
-    it "assigns variables" do
-      @project = create(:project, import_type: 'gitorious', creator_id: user.id)
-      stub_client(repos: [@repo])
-
-      get :status
-
-      expect(assigns(:already_added_projects)).to eq([@project])
-      expect(assigns(:repos)).to eq([@repo])
-    end
-
-    it "does not show already added project" do
-      @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim')
-      stub_client(repos: [@repo])
-
-      get :status
-
-      expect(assigns(:already_added_projects)).to eq([@project])
-      expect(assigns(:repos)).to eq([])
-    end
-  end
-
-  describe "POST create" do
-    before do
-      @repo = Gitlab::GitoriousImport::Repository.new('asd/vim')
-    end
-
-    it "takes already existing namespace" do
-      namespace = create(:namespace, name: "asd", owner: user)
-      expect(Gitlab::GitoriousImport::ProjectCreator).
-        to receive(:new).with(@repo, namespace, user).
-        and_return(double(execute: true))
-      stub_client(repo: @repo)
-
-      post :create, format: :js
-    end
-  end
-end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
deleted file mode 100644
index 2b334ed11725833867af52d1bd3927b1ab9dc477..0000000000000000000000000000000000000000
--- a/spec/controllers/namespaces_controller_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-describe NamespacesController do
-  let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
-
-  describe "GET show" do
-    context "when the namespace belongs to a user" do
-      let!(:other_user) { create(:user) }
-
-      it "redirects to the user's page" do
-        get :show, id: other_user.username
-
-        expect(response).to redirect_to(user_path(other_user))
-      end
-    end
-
-    context "when the namespace belongs to a group" do
-      let!(:group)   { create(:group) }
-
-      context "when the group is public" do
-        context "when not signed in" do
-          it "redirects to the group's page" do
-            get :show, id: group.path
-
-            expect(response).to redirect_to(group_path(group))
-          end
-        end
-
-        context "when signed in" do
-          before do
-            sign_in(user)
-          end
-
-          it "redirects to the group's page" do
-            get :show, id: group.path
-
-            expect(response).to redirect_to(group_path(group))
-          end
-        end
-      end
-
-      context "when the group is private" do
-        before do
-          group.update_attribute(:visibility_level, Group::PRIVATE)
-        end
-
-        context "when not signed in" do
-          it "redirects to the sign in page" do
-            get :show, id: group.path
-            expect(response).to redirect_to(new_user_session_path)
-          end
-        end
-
-        context "when signed in" do
-          before do
-            sign_in(user)
-          end
-
-          context "when the user has access to the group" do
-            before do
-              group.add_developer(user)
-            end
-
-            context "when the user is blocked" do
-              before do
-                user.block
-              end
-
-              it "redirects to the sign in page" do
-                get :show, id: group.path
-
-                expect(response).to redirect_to(new_user_session_path)
-              end
-            end
-
-            context "when the user isn't blocked" do
-              it "redirects to the group's page" do
-                get :show, id: group.path
-
-                expect(response).to redirect_to(group_path(group))
-              end
-            end
-          end
-
-          context "when the user doesn't have access to the group" do
-            it "responds with status 404" do
-              get :show, id: group.path
-
-              expect(response).to have_http_status(404)
-            end
-          end
-        end
-      end
-    end
-
-    context "when the namespace doesn't exist" do
-      context "when signed in" do
-        before do
-          sign_in(user)
-        end
-
-        it "responds with status 404" do
-          get :show, id: "doesntexist"
-
-          expect(response).to have_http_status(404)
-        end
-      end
-
-      context "when not signed in" do
-        it "redirects to the sign in page" do
-          get :show, id: "doesntexist"
-
-          expect(response).to redirect_to(new_user_session_path)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 9444a50b1ce772635c4ef57d791fabae91cc1026..52d13fb6f9e11527c38ff69e12ea873eaf3084f2 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,7 +5,6 @@ describe Projects::BlobController do
   let(:user)    { create(:user) }
 
   before do
-    user = create(:user)
     project.team << [user, :master]
 
     sign_in(user)
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cbe0417a4a760d6742fcf139f5a6410f4c24996f
--- /dev/null
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe Projects::Boards::IssuesController do
+  let(:project) { create(:empty_project) }
+  let(:board)   { create(:board, project: project) }
+  let(:user)    { create(:user) }
+  let(:guest)   { create(:user) }
+
+  let(:planning)    { create(:label, project: project, name: 'Planning') }
+  let(:development) { create(:label, project: project, name: 'Development') }
+
+  let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+  let!(:list2) { create(:list, board: board, label: development, position: 1) }
+
+  before do
+    project.team << [user, :master]
+    project.team << [guest, :guest]
+  end
+
+  describe 'GET index' do
+    context 'with valid list id' do
+      it 'returns issues that have the list label applied' do
+        johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+        issue = create(:labeled_issue, project: project, labels: [planning])
+        create(:labeled_issue, project: project, labels: [planning])
+        create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
+        create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+        issue.subscribe(johndoe)
+
+        list_issues user: user, board: board, list: list2
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(response).to match_response_schema('issues')
+        expect(parsed_response.length).to eq 2
+      end
+    end
+
+    context 'with invalid board id' do
+      it 'returns a not found 404 response' do
+        list_issues user: user, board: 999, list: list2
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with invalid list id' do
+      it 'returns a not found 404 response' do
+        list_issues user: user, board: board, list: 999
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+      end
+
+      it 'returns a forbidden 403 response' do
+        list_issues user: user, board: board, list: list2
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def list_issues(user:, board:, list:)
+      sign_in(user)
+
+      get :index, namespace_id: project.namespace.to_param,
+                  project_id: project.to_param,
+                  board_id: board.to_param,
+                  list_id: list.to_param
+    end
+  end
+
+  describe 'POST create' do
+    context 'with valid params' do
+      it 'returns a successful 200 response' do
+        create_issue user: user, board: board, list: list1, title: 'New issue'
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns the created issue' do
+        create_issue user: user, board: board, list: list1, title: 'New issue'
+
+        expect(response).to match_response_schema('issue')
+      end
+    end
+
+    context 'with invalid params' do
+      context 'when title is nil' do
+        it 'returns an unprocessable entity 422 response' do
+          create_issue user: user, board: board, list: list1, title: nil
+
+          expect(response).to have_http_status(422)
+        end
+      end
+
+      context 'when list does not belongs to project board' do
+        it 'returns a not found 404 response' do
+          list = create(:list)
+
+          create_issue user: user, board: board, list: list, title: 'New issue'
+
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a forbidden 403 response' do
+        create_issue user: guest, board: board, list: list1, title: 'New issue'
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def create_issue(user:, board:, list:, title:)
+      sign_in(user)
+
+      post :create, namespace_id: project.namespace.to_param,
+                    project_id: project.to_param,
+                    board_id: board.to_param,
+                    list_id: list.to_param,
+                    issue: { title: title },
+                    format: :json
+    end
+  end
+
+  describe 'PATCH update' do
+    let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
+
+    context 'with valid params' do
+      it 'returns a successful 200 response' do
+        move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'moves issue to the desired list' do
+        move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(issue.reload.labels).to contain_exactly(development)
+      end
+    end
+
+    context 'with invalid params' do
+      it 'returns a unprocessable entity 422 response for invalid lists' do
+        move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
+
+        expect(response).to have_http_status(422)
+      end
+
+      it 'returns a not found 404 response for invalid board id' do
+        move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns a not found 404 response for invalid issue id' do
+        move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      let(:guest) { create(:user) }
+
+      before do
+        project.team << [guest, :guest]
+      end
+
+      it 'returns a forbidden 403 response' do
+        move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def move(user:, board:, issue:, from_list_id:, to_list_id:)
+      sign_in(user)
+
+      patch :update, namespace_id: project.namespace.to_param,
+                     project_id: project.to_param,
+                     board_id: board.to_param,
+                     id: issue.to_param,
+                     from_list_id: from_list_id,
+                     to_list_id: to_list_id,
+                     format: :json
+    end
+  end
+end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34d6119429d155dd38553c9e22fd2bb51a94344f
--- /dev/null
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -0,0 +1,252 @@
+require 'spec_helper'
+
+describe Projects::Boards::ListsController do
+  let(:project) { create(:empty_project) }
+  let(:board)   { create(:board, project: project) }
+  let(:user)    { create(:user) }
+  let(:guest)   { create(:user) }
+
+  before do
+    project.team << [user, :master]
+    project.team << [guest, :guest]
+  end
+
+  describe 'GET index' do
+    it 'returns a successful 200 response' do
+      read_board_list user: user, board: board
+
+      expect(response).to have_http_status(200)
+      expect(response.content_type).to eq 'application/json'
+    end
+
+    it 'returns a list of board lists' do
+      create(:list, board: board)
+
+      read_board_list user: user, board: board
+
+      parsed_response = JSON.parse(response.body)
+
+      expect(response).to match_response_schema('lists')
+      expect(parsed_response.length).to eq 3
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+      end
+
+      it 'returns a forbidden 403 response' do
+        read_board_list user: user, board: board
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def read_board_list(user:, board:)
+      sign_in(user)
+
+      get :index, namespace_id: project.namespace.to_param,
+                  project_id: project.to_param,
+                  board_id: board.to_param,
+                  format: :json
+    end
+  end
+
+  describe 'POST create' do
+    context 'with valid params' do
+      let(:label) { create(:label, project: project, name: 'Development') }
+
+      it 'returns a successful 200 response' do
+        create_board_list user: user, board: board, label_id: label.id
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns the created list' do
+        create_board_list user: user, board: board, label_id: label.id
+
+        expect(response).to match_response_schema('list')
+      end
+    end
+
+    context 'with invalid params' do
+      context 'when label is nil' do
+        it 'returns a not found 404 response' do
+          create_board_list user: user, board: board, label_id: nil
+
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when label that does not belongs to project' do
+        it 'returns a not found 404 response' do
+          label = create(:label, name: 'Development')
+
+          create_board_list user: user, board: board, label_id: label.id
+
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a forbidden 403 response' do
+        label = create(:label, project: project, name: 'Development')
+
+        create_board_list user: guest, board: board, label_id: label.id
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def create_board_list(user:, board:, label_id:)
+      sign_in(user)
+
+      post :create, namespace_id: project.namespace.to_param,
+                    project_id: project.to_param,
+                    board_id: board.to_param,
+                    list: { label_id: label_id },
+                    format: :json
+    end
+  end
+
+  describe 'PATCH update' do
+    let!(:planning)    { create(:list, board: board, position: 0) }
+    let!(:development) { create(:list, board: board, position: 1) }
+
+    context 'with valid position' do
+      it 'returns a successful 200 response' do
+        move user: user, board: board, list: planning, position: 1
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'moves the list to the desired position' do
+        move user: user, board: board, list: planning, position: 1
+
+        expect(planning.reload.position).to eq 1
+      end
+    end
+
+    context 'with invalid position' do
+      it 'returns an unprocessable entity 422 response' do
+        move user: user, board: board, list: planning, position: 6
+
+        expect(response).to have_http_status(422)
+      end
+    end
+
+    context 'with invalid list id' do
+      it 'returns a not found 404 response' do
+        move user: user, board: board, list: 999, position: 1
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a forbidden 403 response' do
+        move user: guest, board: board, list: planning, position: 6
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def move(user:, board:, list:, position:)
+      sign_in(user)
+
+      patch :update, namespace_id: project.namespace.to_param,
+                     project_id: project.to_param,
+                     board_id: board.to_param,
+                     id: list.to_param,
+                     list: { position: position },
+                     format: :json
+    end
+  end
+
+  describe 'DELETE destroy' do
+    let!(:planning) { create(:list, board: board, position: 0) }
+
+    context 'with valid list id' do
+      it 'returns a successful 200 response' do
+        remove_board_list user: user, board: board, list: planning
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'removes list from board' do
+        expect { remove_board_list user: user, board: board, list: planning }.to change(board.lists, :size).by(-1)
+      end
+    end
+
+    context 'with invalid list id' do
+      it 'returns a not found 404 response' do
+        remove_board_list user: user, board: board, list: 999
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a forbidden 403 response' do
+        remove_board_list user: guest, board: board, list: planning
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def remove_board_list(user:, board:, list:)
+      sign_in(user)
+
+      delete :destroy, namespace_id: project.namespace.to_param,
+                       project_id: project.to_param,
+                       board_id: board.to_param,
+                       id: list.to_param,
+                       format: :json
+    end
+  end
+
+  describe 'POST generate' do
+    context 'when board lists is empty' do
+      it 'returns a successful 200 response' do
+        generate_default_lists user: user, board: board
+
+        expect(response).to have_http_status(200)
+      end
+
+      it 'returns the defaults lists' do
+        generate_default_lists user: user, board: board
+
+        expect(response).to match_response_schema('lists')
+      end
+    end
+
+    context 'when board lists is not empty' do
+      it 'returns an unprocessable entity 422 response' do
+        create(:list, board: board)
+
+        generate_default_lists user: user, board: board
+
+        expect(response).to have_http_status(422)
+      end
+    end
+
+    context 'with unauthorized user' do
+      it 'returns a forbidden 403 response' do
+        generate_default_lists user: guest, board: board
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    def generate_default_lists(user:, board:)
+      sign_in(user)
+
+      post :generate, namespace_id: project.namespace.to_param,
+                      project_id: project.to_param,
+                      board_id: board.to_param,
+                      format: :json
+    end
+  end
+end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc19035740ed50f8ca227b44996b16232cde4433
--- /dev/null
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe Projects::BoardsController do
+  let(:project) { create(:empty_project) }
+  let(:user)    { create(:user) }
+
+  before do
+    project.team << [user, :master]
+    sign_in(user)
+  end
+
+  describe 'GET index' do
+    it 'creates a new project board when project does not have one' do
+      expect { list_boards }.to change(project.boards, :count).by(1)
+    end
+
+    context 'when format is HTML' do
+      it 'renders template' do
+        list_boards
+
+        expect(response).to render_template :index
+        expect(response.content_type).to eq 'text/html'
+      end
+    end
+
+    context 'when format is JSON' do
+      it 'returns a list of project boards' do
+        create_list(:board, 2, project: project)
+
+        list_boards format: :json
+
+        parsed_response = JSON.parse(response.body)
+
+        expect(response).to match_response_schema('boards')
+        expect(parsed_response.length).to eq 2
+      end
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+      end
+
+      it 'returns a not found 404 response' do
+        list_boards
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    def list_boards(format: :html)
+      get :index, namespace_id: project.namespace.to_param,
+                  project_id: project.to_param,
+                  format: format
+    end
+  end
+
+  describe 'GET show' do
+    let!(:board) { create(:board, project: project) }
+
+    context 'when format is HTML' do
+      it 'renders template' do
+        read_board board: board
+
+        expect(response).to render_template :show
+        expect(response.content_type).to eq 'text/html'
+      end
+    end
+
+    context 'when format is JSON' do
+      it 'returns project board' do
+        read_board board: board, format: :json
+
+        expect(response).to match_response_schema('board')
+      end
+    end
+
+    context 'with unauthorized user' do
+      before do
+        allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+        allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+      end
+
+      it 'returns a not found 404 response' do
+        read_board board: board
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when board does not belong to project' do
+      it 'returns a not found 404 response' do
+        another_board = create(:board)
+
+        read_board board: another_board
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    def read_board(board:, format: :html)
+      get :show, namespace_id: project.namespace.to_param,
+                 project_id: project.to_param,
+                 id: board.to_param,
+                 format: format
+    end
+  end
+end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 7e440193d7be1e7a463117b5850a1383d201f02e..646b097d74e0fba6bca594b1c7c63de5f6282c2e 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -102,15 +102,16 @@ describe Projects::CommitController do
     describe "as patch" do
       include_examples "export as", :patch
       let(:format) { :patch }
+      let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
 
       it "is a git email patch" do
-        go(id: commit.id, format: format)
+        go(id: commit2.id, format: format)
 
-        expect(response.body).to start_with("From #{commit.id}")
+        expect(response.body).to start_with("From #{commit2.id}")
       end
 
       it "contains a git diff" do
-        go(id: commit.id, format: format)
+        go(id: commit2.id, format: format)
 
         expect(response.body).to match(/^diff --git/)
       end
@@ -135,6 +136,8 @@ describe Projects::CommitController do
 
   describe "GET branches" do
     it "contains branch and tags information" do
+      commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+
       get(:branches,
           namespace_id: project.namespace.to_param,
           project_id: project.to_param,
@@ -254,16 +257,17 @@ describe Projects::CommitController do
     end
 
     let(:existing_path) { '.gitmodules' }
+    let(:commit2) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
 
     context 'when the commit exists' do
       context 'when the user has access to the project' do
         context 'when the path exists in the diff' do
           it 'enables diff notes' do
-            diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+            diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
 
             expect(assigns(:diff_notes_disabled)).to be_falsey
             expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
-                                                    commit_id: commit.id)
+                                                    commit_id: commit2.id)
           end
 
           it 'only renders the diffs for the path given' do
@@ -272,7 +276,7 @@ describe Projects::CommitController do
               meth.call(diffs)
             end
 
-            diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+            diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
           end
         end
 
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 2518a48e336429b268f65d82bdabb3177db3ada1..1ac7e03a2db1022e5bdc740a160af7202a9c5df6 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -10,15 +10,38 @@ describe Projects::CommitsController do
   end
 
   describe "GET show" do
-    context "as atom feed" do
-      it "renders as atom" do
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project.to_param,
-            id: "master",
-            format: "atom")
-        expect(response).to be_success
-        expect(response.content_type).to eq('application/atom+xml')
+    context "when the ref name ends in .atom" do
+      render_views
+
+      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,
+              id: "master.atom")
+
+          expect(response).to be_success
+          expect(response.content_type).to eq('application/atom+xml')
+        end
+      end
+
+      context "when the ref exists with the suffix" do
+        before do
+          commit = project.repository.commit('master')
+
+          allow_any_instance_of(Repository).to receive(:commit).and_call_original
+          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,
+              id: "master.atom")
+        end
+
+        it "renders as HTML" do
+          expect(response).to be_success
+          expect(response.content_type).to eq('text/html')
+        end
       end
     end
   end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ff617fea8476f23b636c0e5c6e68fb2ecf5156fa
--- /dev/null
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Projects::DiscussionsController do
+  let(:user)    { create(:user) }
+  let(:project) { create(:project) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+  let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+  let(:discussion) { note.discussion }
+
+  let(:request_params) do
+    {
+      namespace_id: project.namespace,
+      project_id: project,
+      merge_request_id: merge_request,
+      id: note.discussion_id
+    }
+  end
+
+  describe 'POST resolve' do
+    before do
+      sign_in user
+    end
+
+    context "when the user is not authorized to resolve the discussion" do
+      it "returns status 404" do
+        post :resolve, request_params
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "when the user is authorized to resolve the discussion" do
+      before do
+        project.team << [user, :developer]
+      end
+
+      context "when the discussion is not resolvable" do
+        before do
+          note.update(system: true)
+        end
+
+        it "returns status 404" do
+          post :resolve, request_params
+
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context "when the discussion is resolvable" do
+        it "resolves the discussion" do
+          post :resolve, request_params
+
+          expect(note.reload.discussion.resolved?).to be true
+          expect(note.reload.discussion.resolved_by).to eq(user)
+        end
+
+        it "sends notifications if all discussions are resolved" do
+          expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+          post :resolve, request_params
+        end
+
+        it "returns the name of the resolving user" do
+          post :resolve, request_params
+
+          expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+        end
+
+        it "returns status 200" do
+          post :resolve, request_params
+
+          expect(response).to have_http_status(200)
+        end
+      end
+    end
+  end
+
+  describe 'DELETE unresolve' do
+    before do
+      sign_in user
+
+      note.discussion.resolve!(user)
+    end
+
+    context "when the user is not authorized to resolve the discussion" do
+      it "returns status 404" do
+        delete :unresolve, request_params
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "when the user is authorized to resolve the discussion" do
+      before do
+        project.team << [user, :developer]
+      end
+
+      context "when the discussion is not resolvable" do
+        before do
+          note.update(system: true)
+        end
+
+        it "returns status 404" do
+          delete :unresolve, request_params
+
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context "when the discussion is resolvable" do
+        it "unresolves the discussion" do
+          delete :unresolve, request_params
+
+          expect(note.reload.discussion.resolved?).to be false
+        end
+
+        it "returns status 200" do
+          delete :unresolve, request_params
+
+          expect(response).to have_http_status(200)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74e6603b0cb5a5bf75e4229f78b5630007a1c2b3
--- /dev/null
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::GraphsController do
+  let(:project) { create(:project) }
+  let(:user)    { create(:user) }
+
+  before do
+    sign_in(user)
+    project.team << [user, :master]
+  end
+
+  describe 'GET #languages' do
+    let(:linguist_repository) do
+      double(languages: {
+               'Ruby'         => 1000,
+               'CoffeeScript' => 350,
+               'PowerShell'   => 15
+             })
+    end
+
+    let(:expected_values) do
+      ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+      [
+        # colors from Linguist:
+        { label: "Ruby",         color: "#701516", highlight: "#701516" },
+        { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+        # colors from SHA256 fallback:
+        { label: "PowerShell",   color: ps_color,  highlight: ps_color  }
+      ]
+    end
+
+    before do
+      allow(Linguist::Repository).to receive(:new).and_return(linguist_repository)
+    end
+
+    it 'sets the correct colour according to language' do
+      get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+      expected_values.each do |val|
+        expect(assigns(:languages)).to include(a_hash_including(val))
+      end
+    end
+  end
+end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index fbe8758dda7333839c1c863c596cb91341faa3a1..b9d9117c928c4a14a51e59c4babba304d03cefed 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -1,8 +1,9 @@
 require 'spec_helper'
 
 describe Projects::GroupLinksController do
-  let(:project) { create(:project, :private) }
   let(:group) { create(:group, :private) }
+  let(:group2) { create(:group, :private) }
+  let(:project) { create(:project, :private, group: group2) }
   let(:user) { create(:user) }
 
   before do
@@ -46,5 +47,39 @@ describe Projects::GroupLinksController do
         expect(group.shared_projects).not_to include project
       end
     end
+
+    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,
+                      link_group_id: group2.id,
+                      link_group_access: ProjectGroupLink.default_access)
+      end
+
+      it 'does not share project with selected group' do
+        expect(group2.shared_projects).not_to include project
+      end
+
+      it 'redirects to project group links page' do
+        expect(response).to redirect_to(
+          namespace_project_group_links_path(project.namespace, project)
+        )
+      end
+    end
+
+    context 'when link group id is not present' do
+      before do
+        post(:create, namespace_id: project.namespace.to_param,
+                      project_id: project.to_param,
+                      link_group_access: ProjectGroupLink.default_access)
+      end
+
+      it 'redirects to project group links page' do
+        expect(response).to redirect_to(
+          namespace_project_group_links_path(project.namespace, project)
+        )
+        expect(flash[:alert]).to eq('Please select a group.')
+      end
+    end
   end
 end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0836b71056c7a7d0e44266b3dbb61ba991e494f0..90419368f2218eba318a7a019e003bfa3737ca7f 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -8,13 +8,13 @@ describe Projects::IssuesController do
   describe "GET #index" do
     context 'external issue tracker' do
       it 'redirects to the external issue tracker' do
-        external = double(issues_url: 'https://example.com/issues')
+        external = double(project_path: 'https://example.com/project')
         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
 
-        expect(response).to redirect_to('https://example.com/issues')
+        expect(response).to redirect_to('https://example.com/project')
       end
     end
 
@@ -370,6 +370,12 @@ describe Projects::IssuesController do
         expect(response).to have_http_status(302)
         expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
       end
+
+      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
+      end
     end
   end
 
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3492b6ffbbb8f686eef5814608744e5ce65deb9d..8faecec006300efa6107f7e4af43f8ad0f91ddbd 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -1,51 +1,98 @@
 require 'spec_helper'
 
 describe Projects::LabelsController do
-  let(:project) { create(:project) }
+  let(:group)   { create(:group) }
+  let(:project) { create(:empty_project, namespace: group) }
   let(:user)    { create(:user) }
 
   before do
     project.team << [user, :master]
+
     sign_in(user)
   end
 
   describe 'GET #index' do
-    def create_label(attributes)
-      create(:label, attributes.merge(project: project))
-    end
+    let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') }
+    let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') }
+    let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') }
+    let!(:label_4) { create(:label, project: project, title: 'Label 4') }
+    let!(:label_5) { create(:label, project: project, title: 'Label 5') }
 
-    before do
-      15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
-      5.times { |i| create_label(title: "label #{100 - i}") }
+    let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') }
+    let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+    let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
+    let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
 
-      get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+    before do
+      create(:label_priority, project: project, label: group_label_1, priority: 3)
+      create(:label_priority, project: project, label: group_label_2, priority: 1)
     end
 
     context '@prioritized_labels' do
-      let(:prioritized_labels) { assigns(:prioritized_labels) }
+      before do
+        list_labels
+      end
 
-      it 'contains only prioritized labels' do
-        expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
+      it 'does not include labels without priority' do
+        list_labels
+
+        expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5)
       end
 
       it 'is sorted by priority, then label title' do
-        priorities_and_titles = prioritized_labels.pluck(:priority, :title)
-
-        expect(priorities_and_titles.sort).to eq(priorities_and_titles)
+        expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2]
       end
     end
 
     context '@labels' do
-      let(:labels) { assigns(:labels) }
+      it 'is sorted by label title' do
+        list_labels
 
-      it 'contains only unprioritized labels' do
-        expect(labels).to all(have_attributes(priority: nil))
+        expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5]
       end
 
-      it 'is sorted by label title' do
-        titles = labels.pluck(:title)
+      it 'does not include labels with priority' do
+        list_labels
+
+        expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2)
+      end
+
+      it 'does not include group labels when project does not belong to a group' do
+        project.update(namespace: create(:namespace))
+
+        list_labels
+
+        expect(assigns(:labels)).not_to include(group_label_3, group_label_4)
+      end
+    end
+
+    def list_labels
+      get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+    end
+  end
+
+  describe 'POST #generate' do
+    let(:admin) { create(:admin) }
+
+    before do
+      sign_in(admin)
+    end
+
+    context 'personal project' do
+      let(:personal_project) { create(:empty_project) }
+
+      it 'creates labels' do
+        post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+
+        expect(response).to have_http_status(302)
+      end
+    end
+
+    context 'project belonging to a group' do
+      it 'creates labels' do
+        post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
 
-        expect(titles.sort).to eq(titles)
+        expect(response).to have_http_status(302)
       end
     end
   end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 69758494543085e94dd493877311407b40b3fec1..49127aecc63e6553077a5d70a80b47f38350e5a7 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,6 +4,11 @@ describe Projects::MergeRequestsController do
   let(:project) { create(:project) }
   let(:user)    { create(:user) }
   let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+  let(:merge_request_with_conflicts) do
+    create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+      mr.mark_as_unmergeable
+    end
+  end
 
   before do
     sign_in(user)
@@ -165,6 +170,35 @@ describe Projects::MergeRequestsController do
         expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
         expect(merge_request.reload.closed?).to be_truthy
       end
+
+      it 'allows editing of a closed merge request' do
+        merge_request.close!
+
+        put :update,
+            namespace_id: project.namespace.path,
+            project_id: project.path,
+            id: merge_request.iid,
+            merge_request: {
+              title: 'New title'
+            }
+
+        expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
+        expect(merge_request.reload.title).to eq 'New title'
+      end
+
+      it 'does not allow to update target branch closed merge request' do
+        merge_request.close!
+
+        put :update,
+            namespace_id: project.namespace.path,
+            project_id: project.path,
+            id: merge_request.iid,
+            merge_request: {
+              target_branch: 'new_branch'
+            }
+
+        expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
+      end
     end
   end
 
@@ -263,6 +297,72 @@ describe Projects::MergeRequestsController do
           end
         end
       end
+
+      describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
+        let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+
+        context 'when enabled' do
+          before do
+            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+          end
+
+          context 'with unresolved discussion' do
+            before do
+              expect(merge_request).not_to be_discussions_resolved
+            end
+
+            it 'returns :failed' do
+              merge_with_sha
+
+              expect(assigns(:status)).to eq(:failed)
+            end
+          end
+
+          context 'with all discussions resolved' do
+            before do
+              merge_request.discussions.each { |d| d.resolve!(user) }
+              expect(merge_request).to be_discussions_resolved
+            end
+
+            it 'returns :success' do
+              merge_with_sha
+
+              expect(assigns(:status)).to eq(:success)
+            end
+          end
+        end
+
+        context 'when disabled' do
+          before do
+            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+          end
+
+          context 'with unresolved discussion' do
+            before do
+              expect(merge_request).not_to be_discussions_resolved
+            end
+
+            it 'returns :success' do
+              merge_with_sha
+
+              expect(assigns(:status)).to eq(:success)
+            end
+          end
+
+          context 'with all discussions resolved' do
+            before do
+              merge_request.discussions.each { |d| d.resolve!(user) }
+              expect(merge_request).to be_discussions_resolved
+            end
+
+            it 'returns :success' do
+              merge_with_sha
+
+              expect(assigns(:status)).to eq(:success)
+            end
+          end
+        end
+      end
     end
   end
 
@@ -286,6 +386,12 @@ describe Projects::MergeRequestsController do
         expect(response).to have_http_status(302)
         expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
       end
+
+      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
+      end
     end
   end
 
@@ -523,4 +629,382 @@ describe Projects::MergeRequestsController do
       end
     end
   end
+
+  describe 'GET conflicts' do
+    let(:json_response) { JSON.parse(response.body) }
+
+    context 'when the conflicts cannot be resolved in the UI' do
+      before do
+        allow_any_instance_of(Gitlab::Conflict::Parser).
+          to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
+
+        get :conflicts,
+            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+            project_id: merge_request_with_conflicts.project.to_param,
+            id: merge_request_with_conflicts.iid,
+            format: 'json'
+      end
+
+      it 'returns a 200 status code' do
+        expect(response).to have_http_status(:ok)
+      end
+
+      it 'returns JSON with a message' do
+        expect(json_response.keys).to contain_exactly('message', 'type')
+      end
+    end
+
+    context 'with valid conflicts' do
+      before do
+        get :conflicts,
+            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+            project_id: merge_request_with_conflicts.project.to_param,
+            id: merge_request_with_conflicts.iid,
+            format: 'json'
+      end
+
+      it 'matches the schema' do
+        expect(response).to match_response_schema('conflicts')
+      end
+
+      it 'includes meta info about the MR' do
+        expect(json_response['commit_message']).to include('Merge branch')
+        expect(json_response['commit_sha']).to match(/\h{40}/)
+        expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
+        expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
+      end
+
+      it 'includes each file that has conflicts' do
+        filenames = json_response['files'].map { |file| file['new_path'] }
+
+        expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
+      end
+
+      it 'splits files into sections with lines' do
+        json_response['files'].each do |file|
+          file['sections'].each do |section|
+            expect(section).to include('conflict', 'lines')
+
+            section['lines'].each do |line|
+              if section['conflict']
+                expect(line['type']).to be_in(['old', 'new'])
+                expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
+              else
+                if line['type'].nil?
+                  expect(line['old_line']).not_to eq(nil)
+                  expect(line['new_line']).not_to eq(nil)
+                else
+                  expect(line['type']).to eq('match')
+                  expect(line['old_line']).to eq(nil)
+                  expect(line['new_line']).to eq(nil)
+                end
+              end
+            end
+          end
+        end
+      end
+
+      it 'has unique section IDs across files' do
+        section_ids = json_response['files'].flat_map do |file|
+          file['sections'].map { |section| section['id'] }.compact
+        end
+
+        expect(section_ids.uniq).to eq(section_ids)
+      end
+    end
+  end
+
+  context 'POST remove_wip' do
+    it 'removes the wip status' do
+      merge_request.title = merge_request.wip_title
+      merge_request.save
+
+      post :remove_wip,
+           namespace_id: merge_request.project.namespace.to_param,
+           project_id: merge_request.project.to_param,
+           id: merge_request.iid
+
+      expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+    end
+  end
+
+  describe 'GET conflict_for_path' do
+    let(:json_response) { JSON.parse(response.body) }
+
+    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,
+          id: merge_request_with_conflicts.iid,
+          old_path: path,
+          new_path: path,
+          format: 'json'
+    end
+
+    context 'when the conflicts cannot be resolved in the UI' do
+      before do
+        allow_any_instance_of(Gitlab::Conflict::Parser).
+          to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
+
+        conflict_for_path('files/ruby/regex.rb')
+      end
+
+      it 'returns a 404 status code' do
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'when the file does not exist cannot be resolved in the UI' do
+      before { conflict_for_path('files/ruby/regexp.rb') }
+
+      it 'returns a 404 status code' do
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'with an existing file' do
+      let(:path) { 'files/ruby/regex.rb' }
+
+      before { conflict_for_path(path) }
+
+      it 'returns a 200 status code' do
+        expect(response).to have_http_status(:ok)
+      end
+
+      it 'returns the file in JSON format' do
+        content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
+
+        expect(json_response).to include('old_path' => path,
+                                         'new_path' => path,
+                                         'blob_icon' => 'file-text-o',
+                                         'blob_path' => a_string_ending_with(path),
+                                         'blob_ace_mode' => 'ruby',
+                                         'content' => content)
+      end
+    end
+  end
+
+  context 'POST resolve_conflicts' do
+    let(:json_response) { JSON.parse(response.body) }
+    let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+
+    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,
+           id: merge_request_with_conflicts.iid,
+           format: 'json',
+           files: files,
+           commit_message: 'Commit message'
+    end
+
+    context 'with valid params' do
+      before do
+        resolved_files = [
+          {
+            'new_path' => 'files/ruby/popen.rb',
+            'old_path' => 'files/ruby/popen.rb',
+            'sections' => {
+              '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+            }
+          }, {
+            'new_path' => 'files/ruby/regex.rb',
+            'old_path' => 'files/ruby/regex.rb',
+            'sections' => {
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+            }
+          }
+        ]
+
+        resolve_conflicts(resolved_files)
+      end
+
+      it 'creates a new commit on the branch' do
+        expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
+        expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
+      end
+
+      it 'returns an OK response' do
+        expect(response).to have_http_status(:ok)
+      end
+    end
+
+    context 'when sections are missing' do
+      before do
+        resolved_files = [
+          {
+            'new_path' => 'files/ruby/popen.rb',
+            'old_path' => 'files/ruby/popen.rb',
+            'sections' => {
+              '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+            }
+          }, {
+            'new_path' => 'files/ruby/regex.rb',
+            'old_path' => 'files/ruby/regex.rb',
+            'sections' => {
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head'
+            }
+          }
+        ]
+
+        resolve_conflicts(resolved_files)
+      end
+
+      it 'returns a 400 error' do
+        expect(response).to have_http_status(:bad_request)
+      end
+
+      it 'has a message with the name of the first missing section' do
+        expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
+      end
+
+      it 'does not create a new commit' do
+        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+      end
+    end
+
+    context 'when files are missing' do
+      before do
+        resolved_files = [
+          {
+            'new_path' => 'files/ruby/regex.rb',
+            'old_path' => 'files/ruby/regex.rb',
+            'sections' => {
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+            }
+          }
+        ]
+
+        resolve_conflicts(resolved_files)
+      end
+
+      it 'returns a 400 error' do
+        expect(response).to have_http_status(:bad_request)
+      end
+
+      it 'has a message with the name of the missing file' do
+        expect(json_response['message']).to include('files/ruby/popen.rb')
+      end
+
+      it 'does not create a new commit' do
+        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+      end
+    end
+
+    context 'when a file has identical content to the conflict' do
+      before do
+        resolved_files = [
+          {
+            'new_path' => 'files/ruby/popen.rb',
+            'old_path' => 'files/ruby/popen.rb',
+            'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
+          }, {
+            'new_path' => 'files/ruby/regex.rb',
+            'old_path' => 'files/ruby/regex.rb',
+            'sections' => {
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+            }
+          }
+        ]
+
+        resolve_conflicts(resolved_files)
+      end
+
+      it 'returns a 400 error' do
+        expect(response).to have_http_status(:bad_request)
+      end
+
+      it 'has a message with the path of the problem file' do
+        expect(json_response['message']).to include('files/ruby/popen.rb')
+      end
+
+      it 'does not create a new commit' do
+        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+      end
+    end
+  end
+
+  describe 'POST assign_related_issues' do
+    let(:issue1) { create(:issue, project: project) }
+    let(:issue2) { create(:issue, project: project) }
+
+    def post_assign_issues
+      merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
+                            author: user,
+                            source_branch: 'feature',
+                            target_branch: 'master')
+
+      post :assign_related_issues,
+           namespace_id: project.namespace.to_param,
+           project_id: project.to_param,
+           id: merge_request.iid
+    end
+
+    it 'shows a flash message on success' do
+      post_assign_issues
+
+      expect(flash[:notice]).to eq '2 issues have been assigned to you'
+    end
+
+    it 'correctly pluralizes flash message on success' do
+      issue2.update!(assignee: user)
+
+      post_assign_issues
+
+      expect(flash[:notice]).to eq '1 issue has been assigned to you'
+    end
+
+    it 'calls MergeRequests::AssignIssuesService' do
+      expect(MergeRequests::AssignIssuesService).to receive(:new).
+        with(project, user, merge_request: merge_request).
+        and_return(double(execute: { count: 1 }))
+
+      post_assign_issues
+    end
+
+    it 'is skipped when not signed in' do
+      project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+      sign_out(:user)
+
+      expect(MergeRequests::AssignIssuesService).not_to receive(:new)
+
+      post_assign_issues
+    end
+  end
+
+  describe 'GET ci_environments_status' do
+    context 'the environment is from a forked project' do
+      let!(:forked)       { create(:project) }
+      let!(:environment)  { create(:environment, project: forked) }
+      let!(:deployment)   { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
+      let(:json_response) { JSON.parse(response.body) }
+      let(:admin)         { create(:admin) }
+
+      let(:merge_request) do
+        create(:forked_project_link, forked_to_project: forked,
+                                     forked_from_project: project)
+
+        create(:merge_request, source_project: forked, target_project: project)
+      end
+
+      before do
+        forked.team << [user, :master]
+
+        get :ci_environments_status,
+          namespace_id: merge_request.project.namespace.to_param,
+          project_id: merge_request.project.to_param,
+          id: merge_request.iid, format: 'json'
+      end
+
+      it 'links to the environment on that project' do
+        expect(json_response.first['url']).to match /#{forked.path_with_namespace}/
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 4e3ef5dc6fa7f303c1c5c1558cf69f14a9562006..7c5f33c63b8f731bbb327434cde4f8fdd4b34da4 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::MilestonesController do
       delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
       expect(response).to be_success
 
-      expect(Event.first.action).to eq(Event::DESTROYED)
+      expect(Event.recent.first.action).to eq(Event::DESTROYED)
 
       expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
       issue.reload
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 75590c1ed4ff97b5ec296c2dd8d2ef2a0f04eab7..92e38b02615f7fd75c590cd36ace370db025d46a 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,4 +1,4 @@
-require('spec_helper')
+require 'spec_helper'
 
 describe Projects::NotesController do
   let(:user)    { create(:user) }
@@ -6,7 +6,15 @@ describe Projects::NotesController do
   let(:issue)   { create(:issue, project: project) }
   let(:note)    { create(:note, noteable: issue, project: project) }
 
-  describe 'POST #toggle_award_emoji' do
+  let(:request_params) do
+    {
+      namespace_id: project.namespace,
+      project_id: project,
+      id: note
+    }
+  end
+
+  describe 'POST toggle_award_emoji' do
     before do
       sign_in(user)
       project.team << [user, :developer]
@@ -14,23 +22,132 @@ describe Projects::NotesController do
 
     it "toggles the award emoji" do
       expect do
-        post(:toggle_award_emoji, namespace_id: project.namespace.path,
-                                  project_id: project.path, id: note.id, name: "thumbsup")
+        post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
       end.to change { note.award_emoji.count }.by(1)
 
       expect(response).to have_http_status(200)
     end
 
     it "removes the already awarded emoji" do
-      post(:toggle_award_emoji, namespace_id: project.namespace.path,
-                                project_id: project.path, id: note.id, name: "thumbsup")
+      post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
 
       expect do
-        post(:toggle_award_emoji, namespace_id: project.namespace.path,
-                                  project_id: project.path, id: note.id, name: "thumbsup")
+        post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
       end.to change { AwardEmoji.count }.by(-1)
 
       expect(response).to have_http_status(200)
     end
   end
+
+  describe "resolving and unresolving" do
+    let(:merge_request) { create(:merge_request, source_project: project) }
+    let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+
+    describe 'POST resolve' do
+      before do
+        sign_in user
+      end
+
+      context "when the user is not authorized to resolve the note" do
+        it "returns status 404" do
+          post :resolve, request_params
+
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context "when the user is authorized to resolve the note" do
+        before do
+          project.team << [user, :developer]
+        end
+
+        context "when the note is not resolvable" do
+          before do
+            note.update(system: true)
+          end
+
+          it "returns status 404" do
+            post :resolve, request_params
+
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context "when the note is resolvable" do
+          it "resolves the note" do
+            post :resolve, request_params
+
+            expect(note.reload.resolved?).to be true
+            expect(note.reload.resolved_by).to eq(user)
+          end
+
+          it "sends notifications if all discussions are resolved" do
+            expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+            post :resolve, request_params
+          end
+
+          it "returns the name of the resolving user" do
+            post :resolve, request_params
+
+            expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+          end
+
+          it "returns status 200" do
+            post :resolve, request_params
+
+            expect(response).to have_http_status(200)
+          end
+        end
+      end
+    end
+
+    describe 'DELETE unresolve' do
+      before do
+        sign_in user
+
+        note.resolve!(user)
+      end
+
+      context "when the user is not authorized to resolve the note" do
+        it "returns status 404" do
+          delete :unresolve, request_params
+
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context "when the user is authorized to resolve the note" do
+        before do
+          project.team << [user, :developer]
+        end
+
+        context "when the note is not resolvable" do
+          before do
+            note.update(system: true)
+          end
+
+          it "returns status 404" do
+            delete :unresolve, request_params
+
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context "when the note is resolvable" do
+          it "unresolves the note" do
+            delete :unresolve, request_params
+
+            expect(note.reload.resolved?).to be false
+          end
+
+          it "returns status 200" do
+            delete :unresolve, request_params
+
+            expect(response).to have_http_status(200)
+          end
+        end
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 5e2a8cf38490ba81d46d6e900ecac8ca0da5be31..2a7523c6512ec364bd8919d8d5e586f3c496a213 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,69 +1,70 @@
 require('spec_helper')
 
 describe Projects::ProjectMembersController do
-  describe '#apply_import' do
-    let(:project) { create(:project) }
-    let(:another_project) { create(:project, :private) }
-    let(:user) { create(:user) }
-    let(:member) { create(:user) }
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
 
-    before do
-      project.team << [user, :master]
-      another_project.team << [member, :guest]
-      sign_in(user)
-    end
+  describe 'GET index' do
+    it 'renders index with 200 status code' do
+      get :index, namespace_id: project.namespace, project_id: project
 
-    shared_context 'import applied' do
-      before do
-        post(:apply_import, namespace_id: project.namespace,
-                            project_id: project,
-                            source_project_id: another_project.id)
-      end
+      expect(response).to have_http_status(200)
+      expect(response).to render_template(:index)
     end
+  end
 
-    context 'when user can access source project members' do
-      before { another_project.team << [user, :guest] }
-      include_context 'import applied'
+  describe 'POST create' do
+    let(:project_user) { create(:user) }
 
-      it 'imports source project members' do
-        expect(project.team_members).to include member
-        expect(response).to set_flash.to 'Successfully imported'
-        expect(response).to redirect_to(
-          namespace_project_project_members_path(project.namespace, project)
-        )
-      end
-    end
+    before { sign_in(user) }
 
-    context 'when user is not member of a source project' do
-      include_context 'import applied'
+    context 'when user does not have enough rights' do
+      before { project.team << [user, :developer] }
 
-      it 'does not import team members' do
-        expect(project.team_members).not_to include member
-      end
+      it 'returns 404' do
+        post :create, namespace_id: project.namespace,
+                      project_id: project,
+                      user_ids: project_user.id,
+                      access_level: Gitlab::Access::GUEST
 
-      it 'responds with not found' do
-        expect(response.status).to eq 404
+        expect(response).to have_http_status(404)
+        expect(project.users).not_to include project_user
       end
     end
-  end
 
-  describe '#index' do
-    context 'when user is member' do
-      before do
-        project = create(:project, :private)
-        member = create(:user)
-        project.team << [member, :guest]
-        sign_in(member)
+    context 'when user has enough rights' do
+      before { project.team << [user, :master] }
+
+      it 'adds user to members' do
+        expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true)
+
+        post :create, namespace_id: project.namespace,
+                      project_id: project,
+                      user_ids: project_user.id,
+                      access_level: Gitlab::Access::GUEST
 
-        get :index, namespace_id: project.namespace, project_id: project
+        expect(response).to set_flash.to 'Users were successfully added.'
+        expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
       end
 
-      it { expect(response).to have_http_status(200) }
+      it 'adds no user to members' do
+        expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false)
+
+        post :create, namespace_id: project.namespace,
+                      project_id: project,
+                      user_ids: '',
+                      access_level: Gitlab::Access::GUEST
+
+        expect(response).to set_flash.to 'No users or groups specified.'
+        expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+      end
     end
   end
 
-  describe '#destroy' do
-    let(:project) { create(:project, :public) }
+  describe 'DELETE destroy' do
+    let(:member) { create(:project_member, :developer, project: project) }
+
+    before { sign_in(user) }
 
     context 'when member is not found' do
       it 'returns 404' do
@@ -76,18 +77,8 @@ describe Projects::ProjectMembersController do
     end
 
     context 'when member is found' do
-      let(:user) { create(:user) }
-      let(:team_user) { create(:user) }
-      let(:member) do
-        project.team << [team_user, :developer]
-        project.members.find_by(user_id: team_user.id)
-      end
-
       context 'when user does not have enough rights' do
-        before do
-          project.team << [user, :developer]
-          sign_in(user)
-        end
+        before { project.team << [user, :developer] }
 
         it 'returns 404' do
           delete :destroy, namespace_id: project.namespace,
@@ -95,15 +86,12 @@ describe Projects::ProjectMembersController do
                            id: member
 
           expect(response).to have_http_status(404)
-          expect(project.users).to include team_user
+          expect(project.members).to include member
         end
       end
 
       context 'when user has enough rights' do
-        before do
-          project.team << [user, :master]
-          sign_in(user)
-        end
+        before { project.team << [user, :master] }
 
         it '[HTML] removes user from members' do
           delete :destroy, namespace_id: project.namespace,
@@ -113,7 +101,7 @@ describe Projects::ProjectMembersController do
           expect(response).to redirect_to(
             namespace_project_project_members_path(project.namespace, project)
           )
-          expect(project.users).not_to include team_user
+          expect(project.members).not_to include member
         end
 
         it '[JS] removes user from members' do
@@ -122,33 +110,27 @@ describe Projects::ProjectMembersController do
                                  id: member
 
           expect(response).to be_success
-          expect(project.users).not_to include team_user
+          expect(project.members).not_to include member
         end
       end
     end
   end
 
-  describe '#leave' do
-    let(:project) { create(:project, :public) }
-    let(:user) { create(:user) }
+  describe 'DELETE leave' do
+    before { sign_in(user) }
 
     context 'when member is not found' do
-      before { sign_in(user) }
-
-      it 'returns 403' do
+      it 'returns 404' do
         delete :leave, namespace_id: project.namespace,
                        project_id: project
 
-        expect(response).to have_http_status(403)
+        expect(response).to have_http_status(404)
       end
     end
 
     context 'when member is found' do
       context 'and is not an owner' do
-        before do
-          project.team << [user, :developer]
-          sign_in(user)
-        end
+        before { project.team << [user, :developer] }
 
         it 'removes user from members' do
           delete :leave, namespace_id: project.namespace,
@@ -161,11 +143,9 @@ describe Projects::ProjectMembersController do
       end
 
       context 'and is an owner' do
-        before do
-          project.update(namespace_id: user.namespace_id)
-          project.team << [user, :master, user]
-          sign_in(user)
-        end
+        let(:project) { create(:project, namespace: user.namespace) }
+
+        before { project.team << [user, :master] }
 
         it 'cannot remove himself from the project' do
           delete :leave, namespace_id: project.namespace,
@@ -176,10 +156,7 @@ describe Projects::ProjectMembersController do
       end
 
       context 'and is a requester' do
-        before do
-          project.request_access(user)
-          sign_in(user)
-        end
+        before { project.request_access(user) }
 
         it 'removes user from members' do
           delete :leave, namespace_id: project.namespace,
@@ -194,13 +171,8 @@ describe Projects::ProjectMembersController do
     end
   end
 
-  describe '#request_access' do
-    let(:project) { create(:project, :public) }
-    let(:user) { create(:user) }
-
-    before do
-      sign_in(user)
-    end
+  describe 'POST request_access' do
+    before { sign_in(user) }
 
     it 'creates a new ProjectMember that is not a team member' do
       post :request_access, namespace_id: project.namespace,
@@ -215,8 +187,10 @@ describe Projects::ProjectMembersController do
     end
   end
 
-  describe '#approve' do
-    let(:project) { create(:project, :public) }
+  describe 'POST approve' do
+    let(:member) { create(:project_member, :access_request, project: project) }
+
+    before { sign_in(user) }
 
     context 'when member is not found' do
       it 'returns 404' do
@@ -229,18 +203,8 @@ describe Projects::ProjectMembersController do
     end
 
     context 'when member is found' do
-      let(:user) { create(:user) }
-      let(:team_requester) { create(:user) }
-      let(:member) do
-        project.request_access(team_requester)
-        project.requesters.find_by(user_id: team_requester.id)
-      end
-
       context 'when user does not have enough rights' do
-        before do
-          project.team << [user, :developer]
-          sign_in(user)
-        end
+        before { project.team << [user, :developer] }
 
         it 'returns 404' do
           post :approve_access_request, namespace_id: project.namespace,
@@ -248,15 +212,12 @@ describe Projects::ProjectMembersController do
                                         id: member
 
           expect(response).to have_http_status(404)
-          expect(project.users).not_to include team_requester
+          expect(project.members).not_to include member
         end
       end
 
       context 'when user has enough rights' do
-        before do
-          project.team << [user, :master]
-          sign_in(user)
-        end
+        before { project.team << [user, :master] }
 
         it 'adds user to members' do
           post :approve_access_request, namespace_id: project.namespace,
@@ -266,9 +227,89 @@ describe Projects::ProjectMembersController do
           expect(response).to redirect_to(
             namespace_project_project_members_path(project.namespace, project)
           )
-          expect(project.users).to include team_requester
+          expect(project.members).to include member
         end
       end
     end
   end
+
+  describe 'POST apply_import' do
+    let(:another_project) { create(:project, :private) }
+    let(:member) { create(:user) }
+
+    before do
+      project.team << [user, :master]
+      another_project.team << [member, :guest]
+      sign_in(user)
+    end
+
+    shared_context 'import applied' do
+      before do
+        post(:apply_import, namespace_id: project.namespace,
+                            project_id: project,
+                            source_project_id: another_project.id)
+      end
+    end
+
+    context 'when user can access source project members' do
+      before { another_project.team << [user, :guest] }
+      include_context 'import applied'
+
+      it 'imports source project members' do
+        expect(project.team_members).to include member
+        expect(response).to set_flash.to 'Successfully imported'
+        expect(response).to redirect_to(
+          namespace_project_project_members_path(project.namespace, project)
+        )
+      end
+    end
+
+    context 'when user is not member of a source project' do
+      include_context 'import applied'
+
+      it 'does not import team members' do
+        expect(project.team_members).not_to include member
+      end
+
+      it 'responds with not found' do
+        expect(response.status).to eq 404
+      end
+    end
+  end
+
+  describe 'POST create' do
+    let(:stranger) { create(:user) }
+
+    context 'when creating owner' do
+      before do
+        project.team << [user, :master]
+        sign_in(user)
+      end
+
+      it 'does not create a member' do
+        expect do
+          post :create, user_ids: stranger.id,
+                        namespace_id: project.namespace,
+                        access_level: Member::OWNER,
+                        project_id: project
+        end.to change { project.members.count }.by(0)
+      end
+    end
+
+    context 'when create master' do
+      before do
+        project.team << [user, :master]
+        sign_in(user)
+      end
+
+      it 'creates a member' do
+        expect do
+          post :create, user_ids: stranger.id,
+                        namespace_id: project.namespace,
+                        access_level: Member::MASTER,
+                        project_id: project
+        end.to change { project.members.count }.by(1)
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 2fe3c2635247dbca8209648d14bc556588ef1941..38e02a466266be1adcc99ce7e7dadd3629ca128a 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::RepositoriesController do
       it 'responds with redirect in correct format' do
         get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
 
-        expect(response.content_type).to start_with 'text/html'
+        expect(response.header["Content-Type"]).to start_with('text/html')
         expect(response).to be_redirect
       end
     end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index cccd492ef0672cd75508fc4024f5edab57657937..2e44b5128b421dcbf4527167112b614eeb484fcf 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -49,4 +49,20 @@ describe Projects::ServicesController do
       let!(:referrer) { nil }
     end
   end
+
+  describe 'PUT #update' do
+    context 'on successful update' do
+      it 'sets the flash' do
+        expect(service).to receive(:to_param).and_return('hipchat')
+
+        put :update,
+          namespace_id: project.namespace.id,
+          project_id: project.id,
+          id: service.id,
+          service: { active: false }
+
+        expect(flash[:notice]).to eq 'Successfully updated.'
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index b8a28f4370716091c782dee31ea1692fcbf6349e..72a3ebf2ebd4cdd2c1517a970a1093267f0822b3 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe Projects::SnippetsController do
-  let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) }
+  let(:project) { create(:project_empty_repo, :public) }
   let(:user)    { create(:user) }
   let(:user2)   { create(:user) }
 
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index a6995145cc19a8a7171adf26634f90257b1157e1..5e661c2c41df35d8314b7579649650eb067d97bb 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -17,4 +17,18 @@ describe Projects::TagsController do
       expect(assigns(:releases)).not_to include(invalid_release)
     end
   end
+
+  describe 'GET show' do
+    before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+
+    context "valid tag" do
+      let(:id) { 'v1.0.0' }
+      it { is_expected.to respond_with(:success) }
+    end
+
+    context "invalid tag" do
+      let(:id) { 'latest' }
+      it { is_expected.to respond_with(:not_found) }
+    end
+  end
 end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 7b3a26d7ca773695830c09c0d26a323d954d799a..19a152bcb055b778a2ba8358e38e50848a382387 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
   end
 
   before do
-    project.team.add_user(user, Gitlab::Access::MASTER)
+    project.add_user(user, Gitlab::Access::MASTER)
     project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
   end
 
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index ffe0641ddd78932daed3f78012dfc1094c4af861..5ddcaa60dc6a39f11ee15c4af557c01fbc11f4e1 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -7,6 +7,26 @@ describe ProjectsController do
   let(:jpg)     { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
   let(:txt)     { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
 
+  describe 'GET index' do
+    context 'as a user' do
+      it 'redirects to root page' do
+        sign_in(user)
+
+        get :index
+
+        expect(response).to redirect_to(root_path)
+      end
+    end
+
+    context 'as a guest' do
+      it 'redirects to Explore page' do
+        get :index
+
+        expect(response).to redirect_to(explore_root_path)
+      end
+    end
+  end
+
   describe "GET show" do
     context "user not project member" do
       before { sign_in(user) }
@@ -41,6 +61,46 @@ describe ProjectsController do
           end
         end
       end
+
+      describe "when project repository is disabled" do
+        render_views
+
+        before do
+          project.team << [user, :developer]
+          project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+        end
+
+        it 'shows wiki homepage' do
+          get :show, namespace_id: project.namespace.path, id: project.path
+
+          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)
+
+          get :show, namespace_id: project.namespace.path, id: project.path
+
+          expect(response).to render_template('projects/issues/_issues')
+        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
+
+          expect(response).to render_template("projects/_customize_workflow")
+        end
+
+        it 'shows activity if enabled by user' do
+          user.update_attribute(:project_view, 'activity')
+
+          get :show, namespace_id: project.namespace.path, id: project.path
+
+          expect(response).to render_template("projects/_activity")
+        end
+      end
     end
 
     context "project with empty repo" do
@@ -63,6 +123,28 @@ describe ProjectsController do
       end
     end
 
+    context "project with broken repo" do
+      let(:empty_project) { create(:project_broken_repo, :public) }
+
+      before { sign_in(user) }
+
+      User.project_views.keys.each do |project_view|
+        context "with #{project_view} view set" do
+          before do
+            user.update_attributes(project_view: project_view)
+
+            get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+          end
+
+          it "renders the empty project view" do
+            allow(Project).to receive(:repo).and_raise(Gitlab::Git::Repository::NoRepository)
+
+            expect(response).to render_template('projects/no_repo')
+          end
+        end
+      end
+    end
+
     context "rendering default project view" do
       render_views
 
@@ -181,6 +263,52 @@ describe ProjectsController do
       expect(response).to have_http_status(302)
       expect(response).to redirect_to(dashboard_projects_path)
     end
+
+    context "when the project is forked" do
+      let(:project)      { create(:project) }
+      let(:fork_project) { create(:project, forked_from_project: project) }
+      let(:merge_request) do
+        create(:merge_request,
+          source_project: fork_project,
+          target_project: project)
+      end
+
+      it "closes all related merge requests" do
+        project.merge_requests << merge_request
+        sign_in(admin)
+
+        delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path
+
+        expect(merge_request.reload.state).to eq('closed')
+      end
+    end
+  end
+
+  describe 'PUT #new_issue_address' do
+    subject do
+      put :new_issue_address,
+        namespace_id: project.namespace.to_param,
+        id: project.to_param
+      user.reload
+    end
+
+    before do
+      sign_in(user)
+      project.team << [user, :developer]
+      allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+    end
+
+    it 'has http status 200' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'changes the user incoming email token' do
+      expect { subject }.to change { user.incoming_email_token }
+    end
+
+    it 'changes projects new issue address' do
+      expect { subject }.to change { project.new_issue_address(user) }
+    end
   end
 
   describe "POST #toggle_star" do
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 9ced397bd4a05864b5dbef191e862341feae7ddc..191e290a11805e2e102f4534d8d5f5271018fe29 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -1,25 +1,108 @@
 require 'rails_helper'
 
 describe SentNotificationsController, type: :controller do
-  let(:user)              { create(:user) }
-  let(:issue)             { create(:issue, author: user) }
-  let(:sent_notification) { create(:sent_notification, noteable: issue) }
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+  let(:sent_notification) { create(:sent_notification, noteable: issue, recipient: user) }
 
-  describe 'GET #unsubscribe' do
-    it 'returns a 404 when calling without existing id' do
-      get(:unsubscribe, id: '0' * 32)
+  let(:issue) do
+    create(:issue, project: project, author: user) do |issue|
+      issue.subscriptions.create(user: user, subscribed: true)
+    end
+  end
+
+  describe 'GET unsubscribe' do
+    context 'when the user is not logged in' do
+      context 'when the force param is passed' do
+        before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+        it 'unsubscribes the user' do
+          expect(issue.subscribed?(user)).to be_falsey
+        end
+
+        it 'sets the flash message' do
+          expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+        end
+
+        it 'redirects to the login page' do
+          expect(response).to redirect_to(new_user_session_path)
+        end
+      end
+
+      context 'when the force param is not passed' do
+        before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+        it 'does not unsubscribe the user' do
+          expect(issue.subscribed?(user)).to be_truthy
+        end
 
-      expect(response.status).to be 404
+        it 'does not set the flash message' do
+          expect(controller).not_to set_flash[:notice]
+        end
+
+        it 'redirects to the login page' do
+          expect(response).to render_template :unsubscribe
+        end
+      end
     end
 
-    context 'calling with id' do
-      it 'shows a flash message to the user' do
-        get(:unsubscribe, id: sent_notification.reply_key)
+    context 'when the user is logged in' do
+      before { sign_in(user) }
+
+      context 'when the ID passed does not exist' do
+        before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
+
+        it 'does not unsubscribe the user' do
+          expect(issue.subscribed?(user)).to be_truthy
+        end
+
+        it 'does not set the flash message' do
+          expect(controller).not_to set_flash[:notice]
+        end
+
+        it 'returns a 404' do
+          expect(response).to have_http_status(:not_found)
+        end
+      end
+
+      context 'when the force param is passed' do
+        before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+        it 'unsubscribes the user' do
+          expect(issue.subscribed?(user)).to be_falsey
+        end
+
+        it 'sets the flash message' do
+          expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+        end
+
+        it 'redirects to the issue page' do
+          expect(response).
+            to redirect_to(namespace_project_issue_path(project.namespace, project, issue))
+        end
+      end
+
+      context 'when the force param is not passed' do
+        let(:merge_request) do
+          create(:merge_request, source_project: project, author: user) do |merge_request|
+            merge_request.subscriptions.create(user: user, subscribed: true)
+          end
+        end
+        let(:sent_notification) { create(:sent_notification, noteable: merge_request, recipient: user) }
+        before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+        it 'unsubscribes the user' do
+          expect(merge_request.subscribed?(user)).to be_falsey
+        end
 
-        expect(response.status).to be 302
+        it 'sets the flash message' do
+          expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+        end
 
-        expect(response).to redirect_to new_user_session_path
-        expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+        it 'redirects to the merge request page' do
+          expect(response).
+            to redirect_to(namespace_project_merge_request_path(project.namespace, project, merge_request))
+        end
       end
     end
   end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 4e9bfb0c69b747d717b458fd8e5a1042b625887a..48d693774616c9170ec8dfd5f39b9afece4e2e40 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -109,6 +109,44 @@ describe SessionsController do
             end
           end
 
+          context 'when the user is on their last attempt' do
+            before do
+              user.update(failed_attempts: User.maximum_attempts.pred)
+            end
+
+            context 'when OTP is valid' do
+              it 'authenticates correctly' do
+                authenticate_2fa(otp_attempt: user.current_otp)
+
+                expect(subject.current_user).to eq user
+              end
+            end
+
+            context 'when OTP is invalid' do
+              before { authenticate_2fa(otp_attempt: 'invalid') }
+
+              it 'does not authenticate' do
+                expect(subject.current_user).not_to eq user
+              end
+
+              it 'warns about invalid login' do
+                expect(response).to set_flash.now[:alert]
+                  .to /Invalid Login or password/
+              end
+
+              it 'locks the user' do
+                expect(user.reload).to be_access_locked
+              end
+
+              it 'keeps the user locked on future login attempts' do
+                post(:create, user: { login: user.username, password: user.password })
+
+                expect(response)
+                  .to set_flash.now[:alert].to /Invalid Login or password/
+              end
+            end
+          end
+
           context 'when another user does not have 2FA enabled' do
             let(:another_user) { create(:user) }
 
@@ -136,6 +174,29 @@ describe SessionsController do
         post(:create, { user: user_params }, { otp_user_id: user.id })
       end
 
+      context 'remember_me field' do
+        it 'sets a remember_user_token cookie when enabled' do
+          allow(U2fRegistration).to receive(:authenticate).and_return(true)
+          allow(controller).to receive(:find_user).and_return(user)
+          expect(controller).
+            to receive(:remember_me).with(user).and_call_original
+
+          authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}")
+
+          expect(response.cookies['remember_user_token']).to be_present
+        end
+
+        it 'does nothing when disabled' do
+          allow(U2fRegistration).to receive(:authenticate).and_return(true)
+          allow(controller).to receive(:find_user).and_return(user)
+          expect(controller).not_to receive(:remember_me)
+
+          authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}")
+
+          expect(response.cookies['remember_user_token']).to be_nil
+        end
+      end
+
       it "creates an audit log record" do
         allow(U2fRegistration).to receive(:authenticate).and_return(true)
         expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 2a89159c0706d272f42ed43f8810f1a5f6f6334f..2d762fdaa04899539137e6ab0e952345970b804c 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -1,9 +1,9 @@
 require 'spec_helper'
 
 describe SnippetsController do
-  describe 'GET #show' do
-    let(:user) { create(:user) }
+  let(:user) { create(:user) }
 
+  describe 'GET #show' do
     context 'when the personal snippet is private' do
       let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
 
@@ -116,117 +116,156 @@ describe SnippetsController do
     end
   end
 
-  describe 'GET #raw' do
-    let(:user) { create(:user) }
+  %w(raw download).each do |action|
+    describe "GET #{action}" do
+      context 'when the personal snippet is private' do
+        let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
 
-    context 'when the personal snippet is private' do
-      let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+        context 'when signed in' do
+          before do
+            sign_in(user)
+          end
 
-      context 'when signed in' do
-        before do
-          sign_in(user)
-        end
+          context 'when signed in user is not the author' do
+            let(:other_author) { create(:author) }
+            let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
 
-        context 'when signed in user is not the author' do
-          let(:other_author) { create(:author) }
-          let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+            it 'responds with status 404' do
+              get action, id: other_personal_snippet.to_param
 
-          it 'responds with status 404' do
-            get :raw, id: other_personal_snippet.to_param
+              expect(response).to have_http_status(404)
+            end
+          end
 
-            expect(response).to have_http_status(404)
+          context 'when signed in user is the author' do
+            before { get action, id: personal_snippet.to_param }
+
+            it 'responds with status 200' do
+              expect(assigns(:snippet)).to eq(personal_snippet)
+              expect(response).to have_http_status(200)
+            end
+
+            it 'has expected headers' do
+              expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+
+              if action == :download
+                expect(response.header['Content-Disposition']).to match(/attachment/)
+              elsif action == :raw
+                expect(response.header['Content-Disposition']).to match(/inline/)
+              end
+            end
           end
         end
 
-        context 'when signed in user is the author' do
-          it 'renders the raw snippet' do
-            get :raw, id: personal_snippet.to_param
+        context 'when not signed in' do
+          it 'redirects to the sign in page' do
+            get action, id: personal_snippet.to_param
 
-            expect(assigns(:snippet)).to eq(personal_snippet)
-            expect(response).to have_http_status(200)
+            expect(response).to redirect_to(new_user_session_path)
           end
         end
       end
 
-      context 'when not signed in' do
-        it 'redirects to the sign in page' do
-          get :raw, id: personal_snippet.to_param
+      context 'when the personal snippet is internal' do
+        let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
 
-          expect(response).to redirect_to(new_user_session_path)
-        end
-      end
-    end
+        context 'when signed in' do
+          before do
+            sign_in(user)
+          end
 
-    context 'when the personal snippet is internal' do
-      let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+          it 'responds with status 200' do
+            get action, id: personal_snippet.to_param
 
-      context 'when signed in' do
-        before do
-          sign_in(user)
+            expect(assigns(:snippet)).to eq(personal_snippet)
+            expect(response).to have_http_status(200)
+          end
         end
 
-        it 'renders the raw snippet' do
-          get :raw, id: personal_snippet.to_param
+        context 'when not signed in' do
+          it 'redirects to the sign in page' do
+            get action, id: personal_snippet.to_param
 
-          expect(assigns(:snippet)).to eq(personal_snippet)
-          expect(response).to have_http_status(200)
+            expect(response).to redirect_to(new_user_session_path)
+          end
         end
       end
 
-      context 'when not signed in' do
-        it 'redirects to the sign in page' do
-          get :raw, id: personal_snippet.to_param
+      context 'when the personal snippet is public' do
+        let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
 
-          expect(response).to redirect_to(new_user_session_path)
-        end
-      end
-    end
+        context 'when signed in' do
+          before do
+            sign_in(user)
+          end
 
-    context 'when the personal snippet is public' do
-      let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+          it 'responds with status 200' do
+            get action, id: personal_snippet.to_param
 
-      context 'when signed in' do
-        before do
-          sign_in(user)
+            expect(assigns(:snippet)).to eq(personal_snippet)
+            expect(response).to have_http_status(200)
+          end
         end
 
-        it 'renders the raw snippet' do
-          get :raw, id: personal_snippet.to_param
+        context 'when not signed in' do
+          it 'responds with status 200' do
+            get action, id: personal_snippet.to_param
 
-          expect(assigns(:snippet)).to eq(personal_snippet)
-          expect(response).to have_http_status(200)
+            expect(assigns(:snippet)).to eq(personal_snippet)
+            expect(response).to have_http_status(200)
+          end
         end
       end
 
-      context 'when not signed in' do
-        it 'renders the raw snippet' do
-          get :raw, id: personal_snippet.to_param
+      context 'when the personal snippet does not exist' do
+        context 'when signed in' do
+          before do
+            sign_in(user)
+          end
 
-          expect(assigns(:snippet)).to eq(personal_snippet)
-          expect(response).to have_http_status(200)
+          it 'responds with status 404' do
+            get action, id: 'doesntexist'
+
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when not signed in' do
+          it 'responds with status 404' do
+            get action, id: 'doesntexist'
+
+            expect(response).to have_http_status(404)
+          end
         end
       end
     end
+  end
 
-    context 'when the personal snippet does not exist' do
-      context 'when signed in' do
-        before do
-          sign_in(user)
-        end
+  context 'award emoji on snippets' do
+    let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+    let(:another_user) { create(:user) }
 
-        it 'responds with status 404' do
-          get :raw, id: 'doesntexist'
+    before do
+      sign_in(another_user)
+    end
 
-          expect(response).to have_http_status(404)
-        end
+    describe 'POST #toggle_award_emoji' do
+      it "toggles the award emoji" do
+        expect do
+          post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+        end.to change { personal_snippet.award_emoji.count }.from(0).to(1)
+
+        expect(response.status).to eq(200)
       end
 
-      context 'when not signed in' do
-        it 'responds with status 404' do
-          get :raw, id: 'doesntexist'
+      it "removes the already awarded emoji" do
+        post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
 
-          expect(response).to have_http_status(404)
-        end
+        expect do
+          post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+        end.to change { personal_snippet.award_emoji.count }.from(1).to(0)
+
+        expect(response.status).to eq(200)
       end
     end
   end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 54a2d3d9460b651bc028dd9b0eb8222d8f6449dd..19a8b1fe52459db34174427a4e9644a14bef9065 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,8 +73,8 @@ describe UsersController do
     end
 
     context 'forked project' do
-      let!(:project) { create(:project) }
-      let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+      let(:project) { create(:project) }
+      let(:forked_project) { Projects::ForkService.new(project, user).execute }
 
       before do
         sign_in(user)
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec46146d9b5f1942beefa9456f957438559a81c0
--- /dev/null
+++ b/spec/factories/boards.rb
@@ -0,0 +1,10 @@
+FactoryGirl.define do
+  factory :board do
+    project factory: :empty_project
+
+    after(:create) do |board|
+      board.lists.create(list_type: :backlog)
+      board.lists.create(list_type: :done)
+    end
+  end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 04d66020c87b74a8359c47ca14bd97e77a65e1a8..ac2a1ba5dffb66bb1f1bffb1d359e5e4377272c8 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: commits
-#
-#  id             :integer          not null, primary key
-#  project_id     :integer
-#  ref            :string(255)
-#  sha            :string(255)
-#  before_sha     :string(255)
-#  push_data      :text
-#  created_at     :datetime
-#  updated_at     :datetime
-#  tag            :boolean          default(FALSE)
-#  yaml_errors    :text
-#  committed_at   :datetime
-#  gl_project_id  :integer
-#
-
 FactoryGirl.define do
   factory :ci_empty_pipeline, class: Ci::Pipeline do
     ref 'master'
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 83fccad679f8de9e1973ca56b63e072a191b9105..3372e5ab685708553720cc96252d9b1b4d232b05 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,14 +1,3 @@
-# == Schema Information
-#
-# Table name: runner_projects
-#
-#  id         :integer          not null, primary key
-#  runner_id  :integer          not null
-#  project_id :integer          not null
-#  created_at :datetime
-#  updated_at :datetime
-#
-
 FactoryGirl.define do
   factory :ci_runner_project, class: Ci::RunnerProject do
     runner_id 1
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 5b645fab32ee5993ff00a67a1a0bd9390be69338..e3b73e29987514dc89ef26e7f9ae512151bde743 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: runners
-#
-#  id           :integer          not null, primary key
-#  token        :string(255)
-#  created_at   :datetime
-#  updated_at   :datetime
-#  description  :string(255)
-#  contacted_at :datetime
-#  active       :boolean          default(TRUE), not null
-#  is_shared    :boolean          default(FALSE)
-#  name         :string(255)
-#  version      :string(255)
-#  revision     :string(255)
-#  platform     :string(255)
-#  architecture :string(255)
-#
-
 FactoryGirl.define do
   factory :ci_runner, class: Ci::Runner do
     sequence :description do |n|
@@ -30,5 +11,9 @@ FactoryGirl.define do
     trait :shared do
       is_shared true
     end
+
+    trait :inactive do
+      active false
+    end
   end
 end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 856a8e725eb94b56a5d9464ad101ee50c60b95af..6653f0bb5c3897578b8b411798f4f476ac2e1c5d 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -1,17 +1,3 @@
-# == Schema Information
-#
-# Table name: ci_variables
-#
-#  id                   :integer          not null, primary key
-#  project_id           :integer          not null
-#  key                  :string(255)
-#  value                :text
-#  encrypted_value      :text
-#  encrypted_value_salt :string(255)
-#  encrypted_value_iv   :string(255)
-#  gl_project_id        :integer
-#
-
 FactoryGirl.define do
   factory :ci_variable, class: Ci::Variable do
     sequence(:key) { |n| "VARIABLE_#{n}" }
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 82591604fcb313c77789f7d0117c2f98a1df56c8..6f24bf58d14085b18a4d4b6b50163c60a7fdf4a7 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,11 +3,12 @@ FactoryGirl.define do
     sha '97de212e80737a608d939f648d959671fb0a0142'
     ref 'master'
     tag false
+    project nil
 
     environment factory: :environment
 
     after(:build) do |deployment, evaluator|
-      deployment.project = deployment.environment.project
+      deployment.project ||= deployment.environment.project
     end
   end
 end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 90788f30ac9b8dfdbfd3affa4acce6af55ec862f..8820d527c6195601538bd354c526e148a753dac2 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,10 +1,11 @@
 FactoryGirl.define do
   factory :event do
+    project
+    author factory: :user
+
     factory :closed_issue_event do
-      project
       action { Event::CLOSED }
       target factory: :closed_issue
-      author factory: :user
     end
   end
 end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index debb86d997f48773b6a323ea08d9228e6f80f883..080b2e75ea167872b9b2dbaf0208f5529cc970d7 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -1,20 +1,14 @@
-# == Schema Information
-#
-# Table name: group_members
-#
-#  id                 :integer          not null, primary key
-#  group_access       :integer          not null
-#  group_id           :integer          not null
-#  user_id            :integer          not null
-#  created_at         :datetime
-#  updated_at         :datetime
-#  notification_level :integer          default(3), not null
-#
-
 FactoryGirl.define do
   factory :group_member do
     access_level { GroupMember::OWNER }
     group
     user
+
+    trait(:guest)     { access_level GroupMember::GUEST }
+    trait(:reporter)  { access_level GroupMember::REPORTER }
+    trait(:developer) { access_level GroupMember::DEVELOPER }
+    trait(:master)    { access_level GroupMember::MASTER }
+    trait(:owner)     { access_level GroupMember::OWNER }
+    trait(:access_request) { requested_at Time.now }
   end
 end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 2c0a2dd94ca1bfa60ac75eda28d6d49557cf8323..2b4670be4689ae6fc8c03e0d31b1c22060f82706 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,4 +1,8 @@
 FactoryGirl.define do
+  sequence :issue_created_at do |n|
+    4.hours.ago + ( 2 * n ).seconds
+  end
+
   factory :issue do
     title
     author
diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f25939d2d3eebb9e482e4de3d473a29ace74df7f
--- /dev/null
+++ b/spec/factories/label_priorities.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+  factory :label_priority do
+    project factory: :empty_project
+    label
+    sequence(:priority)
+  end
+end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index eb489099854d719e3752cf6e47701e3fc66f3ce2..3e8822faf972cff162261650b4f06f51a48ddeb6 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -1,7 +1,23 @@
 FactoryGirl.define do
-  factory :label do
+  factory :label, class: ProjectLabel do
     sequence(:title) { |n| "label#{n}" }
     color "#990000"
     project
+
+    transient do
+      priority nil
+    end
+
+    after(:create) do |label, evaluator|
+      if evaluator.priority
+        label.priorities.create(project: label.project, priority: evaluator.priority)
+      end
+    end
+  end
+
+  factory :group_label, class: GroupLabel do
+    sequence(:title) { |n| "label#{n}" }
+    color "#990000"
+    group
   end
 end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e3f06c682cdc75ef6465e55614ee71279ec0340
--- /dev/null
+++ b/spec/factories/lists.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+  factory :list do
+    board
+    label
+    list_type :label
+    sequence(:position)
+  end
+
+  factory :backlog_list, parent: :list do
+    list_type :backlog
+    label nil
+    position nil
+  end
+
+  factory :done_list, parent: :list do
+    list_type :done
+    label nil
+    position nil
+  end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index c6a08d78b78f6242a27c1353cecedf8994feaafa..37eb49c94df104885d82cd3f1b6cf66da5163dad 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -68,5 +68,20 @@ FactoryGirl.define do
     factory :closed_merge_request, traits: [:closed]
     factory :reopened_merge_request, traits: [:reopened]
     factory :merge_request_with_diffs, traits: [:with_diffs]
+    factory :merge_request_with_diff_notes do
+      after(:create) do |mr|
+        create(:diff_note_on_merge_request, noteable: mr, project: mr.source_project)
+      end
+    end
+
+    factory :labeled_merge_request do
+      transient do
+        labels []
+      end
+
+      after(:create) do |merge_request, evaluator|
+        merge_request.update_attributes(labels: evaluator.labels)
+      end
+    end
   end
 end
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index e9e85962fe4e6d30b5cfdfed3f8a6e8b767e1df5..84da71ed6dcce196a759e92d6e14fd73911a874a 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -3,10 +3,15 @@ FactoryGirl.define do
     title
     project
 
+    trait :active do
+      state "active"
+    end
+
     trait :closed do
-      state :closed
+      state "closed"
     end
 
+    factory :active_milestone, traits: [:active]
     factory :closed_milestone, traits: [:closed]
   end
 end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 83e38095febed99cdbce6c06bb20168917117518..6919002dedcd439a03f34a9a077916b7272071cd 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -28,6 +28,11 @@ FactoryGirl.define do
           diff_refs: noteable.diff_refs
         )
       end
+
+      trait :resolved do
+        resolved_at { Time.now }
+        resolved_by { create(:user) }
+      end
     end
 
     factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 4fd51a2349060aa4ccc53f96720dbc66deba525e..424ecc65759c563bf8562470c9f18e617167fcb4 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -14,6 +14,7 @@ FactoryGirl.define do
       note_events true
       build_events true
       pipeline_events true
+      wiki_page_events true
     end
   end
 end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index cf3659ba2750654a6086ba48cc7d0d2ccd698a24..c21927640d10736e71710c3e2d50293ab8e97507 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -4,24 +4,10 @@ FactoryGirl.define do
     project
     master
 
-    trait :guest do
-      access_level ProjectMember::GUEST
-    end
-
-    trait :reporter do
-      access_level ProjectMember::REPORTER
-    end
-
-    trait :developer do
-      access_level ProjectMember::DEVELOPER
-    end
-
-    trait :master do
-      access_level ProjectMember::MASTER
-    end
-
-    trait :owner do
-      access_level ProjectMember::OWNER
-    end
+    trait(:guest)     { access_level ProjectMember::GUEST }
+    trait(:reporter)  { access_level ProjectMember::REPORTER }
+    trait(:developer) { access_level ProjectMember::DEVELOPER }
+    trait(:master)    { access_level ProjectMember::MASTER }
+    trait(:access_request) { requested_at Time.now }
   end
 end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b682ced75acf77977d07a662fdc41b76dcb0c063..bfd88a254f1c10e193872ba26236c9df3de8fde3 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -8,7 +8,9 @@ FactoryGirl.define do
     path { name.downcase.gsub(/\s/, '_') }
     namespace
     creator
-    snippets_enabled true
+
+    # Behaves differently to nil due to cache_has_external_issue_tracker
+    has_external_issue_tracker false
 
     trait :public do
       visibility_level Gitlab::VisibilityLevel::PUBLIC
@@ -27,6 +29,40 @@ FactoryGirl.define do
         project.create_repository
       end
     end
+
+    trait :broken_repo do
+      after(:create) do |project|
+        project.create_repository
+
+        FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
+      end
+    end
+
+    # Nest Project Feature attributes
+    transient do
+      wiki_access_level ProjectFeature::ENABLED
+      builds_access_level ProjectFeature::ENABLED
+      snippets_access_level ProjectFeature::ENABLED
+      issues_access_level ProjectFeature::ENABLED
+      merge_requests_access_level ProjectFeature::ENABLED
+      repository_access_level ProjectFeature::ENABLED
+    end
+
+    after(:create) do |project, evaluator|
+      # Builds and MRs can't have higher visibility level than repository access level.
+      builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
+      merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
+
+      project.project_feature.
+        update_attributes!(
+          wiki_access_level: evaluator.wiki_access_level,
+          builds_access_level: builds_access_level,
+          snippets_access_level: evaluator.snippets_access_level,
+          issues_access_level: evaluator.issues_access_level,
+          merge_requests_access_level: merge_requests_access_level,
+          repository_access_level: evaluator.repository_access_level
+        )
+    end
   end
 
   # Project with empty repository
@@ -37,6 +73,13 @@ FactoryGirl.define do
     empty_repo
   end
 
+  # Project with broken repository
+  #
+  # Project with an invalid repository state
+  factory :project_broken_repo, parent: :empty_project do
+    broken_repo
+  end
+
   # Project with test repository
   #
   # Test repository source can be found at
@@ -58,6 +101,8 @@ FactoryGirl.define do
   end
 
   factory :redmine_project, parent: :project do
+    has_external_issue_tracker true
+
     after :create do |project|
       project.create_redmine_service(
         active: true,
@@ -71,14 +116,15 @@ FactoryGirl.define do
   end
 
   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',
-          'project_url'   => 'http://jira.example/issues/?jql=project=A',
-          'issues_url'    => 'http://jira.example/browse/:id',
-          'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
+          title: 'JIRA tracker',
+          url: 'http://jira.example.net',
+          project_key: 'JIRA'
         }
       )
     end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 2f82fafc13aaaf0a62fd79afb1f3d10943c3869e..d92c66b689d363eddec190a9078930ca8ab39c4a 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -7,15 +7,16 @@ describe "Admin Runners" do
 
   describe "Runners page" do
     before do
-      runner = FactoryGirl.create(:ci_runner)
+      runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now)
       pipeline = FactoryGirl.create(:ci_pipeline)
       FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
       visit admin_runners_path
     end
 
-    it { page.has_text? "Manage Runners" }
-    it { page.has_text? "To register a new runner" }
-    it { page.has_text? "Runners with last contact less than a minute ago: 1" }
+    it 'has all necessary texts' do
+      expect(page).to have_text "To register a new Runner"
+      expect(page).to have_text "Runners with last contact less than a minute ago: 1"
+    end
 
     describe 'search' do
       before do
@@ -27,8 +28,10 @@ describe "Admin Runners" do
         search_form.click_button 'Search'
       end
 
-      it { expect(page).to have_content("runner-foo") }
-      it { expect(page).not_to have_content("runner-bar") }
+      it 'shows correct runner' do
+        expect(page).to have_content("runner-foo")
+        expect(page).not_to have_content("runner-bar")
+      end
     end
   end
 
@@ -46,8 +49,10 @@ describe "Admin Runners" do
     end
 
     describe 'projects' do
-      it { expect(page).to have_content(@project1.name_with_namespace) }
-      it { expect(page).to have_content(@project2.name_with_namespace) }
+      it 'contains project names' do
+        expect(page).to have_content(@project1.name_with_namespace)
+        expect(page).to have_content(@project2.name_with_namespace)
+      end
     end
 
     describe 'search' do
@@ -57,8 +62,10 @@ describe "Admin Runners" do
         search_form.click_button 'Search'
       end
 
-      it { expect(page).to have_content(@project1.name_with_namespace) }
-      it { expect(page).not_to have_content(@project2.name_with_namespace) }
+      it 'contains name of correct project' do
+        expect(page).to have_content(@project1.name_with_namespace)
+        expect(page).not_to have_content(@project2.name_with_namespace)
+      end
     end
 
     describe 'enable/create' do
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index f4e5c26b51918579b76041fd35ae40d7f8d973e6..1df972843e2e9c3e678b943b48ec1f006d2bc98c 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -6,12 +6,49 @@ describe 'Admin System Info' do
   end
 
   describe 'GET /admin/system_info' do
-    it 'shows system info page' do
-      visit admin_system_info_path
+    let(:cpu) { double(:cpu, length: 2) }
+    let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) }
 
-      expect(page).to have_content 'CPU'
-      expect(page).to have_content 'Memory'
-      expect(page).to have_content 'Disks'
+    context 'when all info is available' do
+      before do
+        allow(Vmstat).to receive(:cpu).and_return(cpu)
+        allow(Vmstat).to receive(:memory).and_return(memory)
+        visit admin_system_info_path
+      end
+
+      it 'shows system info page' do
+        expect(page).to have_content 'CPU 2 cores'
+        expect(page).to have_content 'Memory 4 GB / 16 GB'
+        expect(page).to have_content 'Disks'
+      end
+    end
+
+    context 'when CPU info is not available' do
+      before do
+        allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT)
+        allow(Vmstat).to receive(:memory).and_return(memory)
+        visit admin_system_info_path
+      end
+
+      it 'shows system info page with no CPU info' do
+        expect(page).to have_content 'CPU Unable to collect CPU info'
+        expect(page).to have_content 'Memory 4 GB / 16 GB'
+        expect(page).to have_content 'Disks'
+      end
+    end
+
+    context 'when memory info is not available' do
+      before do
+        allow(Vmstat).to receive(:cpu).and_return(cpu)
+        allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT)
+        visit admin_system_info_path
+      end
+
+      it 'shows system info page with no CPU info' do
+        expect(page).to have_content 'CPU 2 cores'
+        expect(page).to have_content 'Memory Unable to collect memory info'
+        expect(page).to have_content 'Disks'
+      end
     end
   end
 end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 4dd9548cfc51737649d28629e63851f771c4f7d7..21ee6cedbae213fa9b69029d9ec547da9a977218 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -19,6 +19,17 @@ describe "Dashboard Issues Feed", feature: true  do
         expect(body).to have_selector('title', text: "#{user.name} issues")
       end
 
+      it "renders atom feed with url parameters" 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)
+
+        expect(params).to include('private_token' => [user.private_token])
+        expect(params).to include('state' => ['opened'])
+        expect(params).to include('assignee_id' => [user.id.to_s])
+      end
+
       context "issue with basic fields" do
         let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
 
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 09c140868fb818bdbc5adb966081040ed09c15fd..863412d18eb7dc36ffec875de38b4638cc47c08a 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -3,10 +3,14 @@ require 'spec_helper'
 describe 'Issues Feed', feature: true  do
   describe 'GET /issues' do
     let!(:user)     { create(:user) }
+    let!(:group)    { create(:group) }
     let!(:project)  { create(:project) }
     let!(:issue)    { create(:issue, author: user, project: project) }
 
-    before { project.team << [user, :developer] }
+    before do
+      project.team << [user, :developer]
+      group.add_developer(user)
+    end
 
     context 'when authenticated' do
       it 'renders atom feed' do
@@ -33,5 +37,28 @@ describe 'Issues Feed', feature: true  do
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
+
+    it "renders atom feed with url parameters for project issues" do
+      visit namespace_project_issues_path(project.namespace, project,
+                                          :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)
+
+      expect(params).to include('private_token' => [user.private_token])
+      expect(params).to include('state' => ['opened'])
+      expect(params).to include('assignee_id' => [user.id.to_s])
+    end
+
+    it "renders atom feed with url parameters for group issues" 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)
+
+      expect(params).to include('private_token' => [user.private_token])
+      expect(params).to include('state' => ['opened'])
+      expect(params).to include('assignee_id' => [user.id.to_s])
+    end
   end
 end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index a8833194421fca4eb355e158ad1c1a2cf8935aa9..f8c3ccb416b022a802d7438e728233f6877f96a1 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -53,7 +53,7 @@ describe "User Feed", feature: true  do
       end
 
       it 'has XHTML summaries in issue descriptions' do
-        expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
+        expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/
       end
 
       it 'has XHTML summaries in notes' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6cb8753e8fc6797aa71a271881408cdd8a4be221
--- /dev/null
+++ b/spec/features/boards/boards_spec.rb
@@ -0,0 +1,709 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+  include WaitForAjax
+  include WaitForVueResource
+
+  let(:project) { create(:empty_project, :public) }
+  let(:board)   { create(:board, project: project) }
+  let(:user)    { create(:user) }
+  let!(:user2)  { create(:user) }
+
+  before do
+    project.team << [user, :master]
+    project.team << [user2, :master]
+
+    login_as(user)
+  end
+
+  context 'no lists' do
+    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 'shows blank state' do
+      expect(page).to have_content('Welcome to your Issue Board!')
+    end
+
+    it 'hides the blank state when clicking nevermind button' do
+      page.within(find('.board-blank-state')) do
+        click_button("Nevermind, I'll use my own")
+      end
+      expect(page).to have_selector('.board', count: 2)
+    end
+
+    it 'creates default lists' do
+      lists = ['Backlog', 'To Do', 'Doing', 'Done']
+
+      page.within(find('.board-blank-state')) do
+        click_button('Add default lists')
+      end
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 4)
+
+      page.all('.board').each_with_index do |list, i|
+        expect(list.find('.board-title')).to have_content(lists[i])
+      end
+    end
+  end
+
+  context 'with lists' do
+    let(:milestone) { create(:milestone, project: project) }
+
+    let(:planning)    { create(:label, project: project, name: 'Planning', description: 'Test') }
+    let(:development) { create(:label, project: project, name: 'Development') }
+    let(:testing)     { create(:label, project: project, name: 'Testing') }
+    let(:bug)         { create(:label, project: project, name: 'Bug') }
+    let!(:backlog)    { create(:label, project: project, name: 'Backlog') }
+    let!(:done)       { create(:label, project: project, name: 'Done') }
+    let!(:accepting)  { create(:label, project: project, name: 'Accepting Merge Requests') }
+
+    let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+    let!(:list2) { create(:list, board: board, label: development, position: 1) }
+
+    let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+    let!(:issue1) { create(:issue, project: project, assignee: user) }
+    let!(:issue2) { create(:issue, project: project, author: user2) }
+    let!(:issue3) { create(:issue, project: project) }
+    let!(:issue4) { create(:issue, project: project) }
+    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!(:issue8) { create(:closed_issue, project: project) }
+    let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
+
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 4)
+      expect(find('.board:nth-child(1)')).to have_selector('.card')
+      expect(find('.board:nth-child(2)')).to have_selector('.card')
+      expect(find('.board:nth-child(3)')).to have_selector('.card')
+      expect(find('.board:nth-child(4)')).to have_selector('.card')
+    end
+
+    it 'shows lists' do
+      expect(page).to have_selector('.board', count: 4)
+    end
+
+    it 'shows description tooltip on list title' do
+      page.within('.board:nth-child(2)') do
+        expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
+      end
+    end
+
+    it 'shows issues in lists' do
+      wait_for_board_cards(2, 2)
+      wait_for_board_cards(3, 2)
+    end
+
+    it 'shows confidential issues with icon' do
+      page.within(find('.board', match: :first)) do
+        expect(page).to have_selector('.confidential-icon', count: 1)
+      end
+    end
+
+    it 'search backlog list' do
+      page.within('#js-boards-seach') do
+        find('.form-control').set(issue1.title)
+      end
+
+      wait_for_vue_resource
+
+      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+    end
+
+    it 'search done list' do
+      page.within('#js-boards-seach') do
+        find('.form-control').set(issue8.title)
+      end
+
+      wait_for_vue_resource
+
+      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+    end
+
+    it 'search list' do
+      page.within('#js-boards-seach') do
+        find('.form-control').set(issue5.title)
+      end
+
+      wait_for_vue_resource
+
+      expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+      expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+      expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+    end
+
+    it 'allows user to delete board' do
+      page.within(find('.board:nth-child(2)')) do
+        find('.board-delete').click
+      end
+
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 3)
+    end
+
+    it 'removes checkmark in new list dropdown after deleting' do
+      click_button 'Create new list'
+      wait_for_ajax
+
+      page.within(find('.board:nth-child(2)')) do
+        find('.board-delete').click
+      end
+
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 3)
+      expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+    end
+
+    it 'infinite scrolls list' do
+      50.times do
+        create(:issue, project: project)
+      end
+
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+
+      page.within(find('.board', match: :first)) do
+        expect(page.find('.board-header')).to have_content('56')
+        expect(page).to have_selector('.card', count: 20)
+        expect(page).to have_content('Showing 20 of 56 issues')
+
+        evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+        wait_for_vue_resource
+
+        expect(page).to have_selector('.card', count: 40)
+        expect(page).to have_content('Showing 40 of 56 issues')
+
+        evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+        wait_for_vue_resource
+
+        expect(page).to have_selector('.card', count: 56)
+        expect(page).to have_content('Showing all issues')
+      end
+    end
+
+    context 'backlog' do
+      it 'shows issues in backlog with no labels' do
+        wait_for_board_cards(1, 6)
+      end
+
+      it 'moves issue from backlog into list' do
+        drag_to(list_to_index: 1)
+
+        wait_for_vue_resource
+        wait_for_board_cards(1, 5)
+        wait_for_board_cards(2, 3)
+      end
+    end
+
+    context 'done' do
+      it 'shows list of done issues' do
+        wait_for_board_cards(4, 1)
+        wait_for_ajax
+      end
+
+      it 'moves issue to done' do
+        drag_to(list_from_index: 0, list_to_index: 3)
+
+        wait_for_board_cards(1, 5)
+        wait_for_board_cards(2, 2)
+        wait_for_board_cards(3, 2)
+        wait_for_board_cards(4, 2)
+
+        expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
+        expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+        expect(find('.board:nth-child(4)')).to have_content(issue9.title)
+        expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+      end
+
+      it 'removes all of the same issue to done' do
+        drag_to(list_from_index: 1, list_to_index: 3)
+
+        wait_for_board_cards(1, 6)
+        wait_for_board_cards(2, 1)
+        wait_for_board_cards(3, 1)
+        wait_for_board_cards(4, 2)
+
+        expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
+        expect(find('.board:nth-child(4)')).to have_content(issue6.title)
+        expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+      end
+    end
+
+    context 'lists' do
+      it 'changes position of list' do
+        drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+
+        wait_for_board_cards(1, 6)
+        wait_for_board_cards(2, 2)
+        wait_for_board_cards(3, 2)
+        wait_for_board_cards(4, 1)
+
+        expect(find('.board:nth-child(2)')).to have_content(development.title)
+        expect(find('.board:nth-child(2)')).to have_content(planning.title)
+      end
+
+      it 'issue moves between lists' do
+        drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+
+        wait_for_board_cards(1, 6)
+        wait_for_board_cards(2, 1)
+        wait_for_board_cards(3, 3)
+        wait_for_board_cards(4, 1)
+
+        expect(find('.board:nth-child(3)')).to have_content(issue6.title)
+        expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+      end
+
+      it 'issue moves between lists' do
+        drag_to(list_from_index: 2, list_to_index: 1)
+
+        wait_for_board_cards(1, 6)
+        wait_for_board_cards(2, 3)
+        wait_for_board_cards(3, 1)
+        wait_for_board_cards(4, 1)
+
+        expect(find('.board:nth-child(2)')).to have_content(issue7.title)
+        expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+      end
+
+      it 'issue moves from done' do
+        drag_to(list_from_index: 3, list_to_index: 1)
+
+        expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+
+        wait_for_board_cards(1, 6)
+        wait_for_board_cards(2, 3)
+        wait_for_board_cards(3, 2)
+        wait_for_board_cards(4, 0)
+      end
+
+      context 'issue card' do
+        it 'shows assignee' do
+          page.within(find('.board', match: :first)) do
+            expect(page).to have_selector('.avatar', count: 1)
+          end
+        end
+      end
+
+      context 'new list' do
+        it 'shows all labels in new list dropdown' do
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            expect(page).to have_content(planning.title)
+            expect(page).to have_content(development.title)
+            expect(page).to have_content(testing.title)
+          end
+        end
+
+        it 'creates new list for label' do
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            click_link testing.title
+          end
+
+          wait_for_vue_resource
+
+          expect(page).to have_selector('.board', count: 5)
+        end
+
+        it 'creates new list for Backlog label' do
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            click_link backlog.title
+          end
+
+          wait_for_vue_resource
+
+          expect(page).to have_selector('.board', count: 5)
+        end
+
+        it 'creates new list for Done label' do
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            click_link done.title
+          end
+
+          wait_for_vue_resource
+
+          expect(page).to have_selector('.board', count: 5)
+        end
+
+        it 'keeps dropdown open after adding new list' do
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            click_link done.title
+          end
+
+          wait_for_vue_resource
+
+          expect(find('.issue-boards-search')).to have_selector('.open')
+        end
+
+        it 'moves issues from backlog into new list' do
+          wait_for_board_cards(1, 6)
+
+          click_button 'Create new list'
+          wait_for_ajax
+
+          page.within('.dropdown-menu-issues-board-new') do
+            click_link testing.title
+          end
+
+          wait_for_vue_resource
+
+          wait_for_board_cards(1, 5)
+        end
+
+        it 'creates new list from a new label' do
+          click_button 'Create new list'
+
+          wait_for_ajax
+
+          click_link 'Create new label'
+
+          fill_in('new_label_name', with: 'Testing New Label')
+
+          first('.suggest-colors a').click
+
+          click_button 'Create'
+
+          wait_for_ajax
+          wait_for_vue_resource
+
+          expect(page).to have_selector('.board', count: 5)
+        end
+      end
+    end
+
+    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
+
+        wait_for_vue_resource
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..4))
+      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
+
+        wait_for_vue_resource
+
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..4))
+      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
+
+        wait_for_vue_resource
+        wait_for_board_cards(1, 0)
+        wait_for_board_cards(2, 1)
+        wait_for_board_cards(3, 0)
+        wait_for_board_cards(4, 0)
+      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
+
+        wait_for_vue_resource
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..4))
+      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
+
+        # Test after reload
+        page.evaluate_script 'window.location.reload()'
+
+        wait_for_vue_resource
+
+        page.within(find('.board', match: :first)) do
+          expect(page.find('.board-header')).to have_content('1')
+          expect(page).to have_selector('.card', count: 1)
+        end
+
+        page.within(find('.board:nth-child(2)')) do
+          expect(page.find('.board-header')).to have_content('0')
+          expect(page).to have_selector('.card', count: 0)
+        end
+      end
+
+      it 'removes filtered labels' do
+        wait_for_vue_resource
+
+        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)
+
+          page.within '.dropdown-menu-labels' do
+            click_link(testing.title)
+            wait_for_vue_resource(spinner: false)
+          end
+
+          expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
+        end
+      end
+
+      it 'infinite scrolls list with label filter' do
+        50.times do
+          create(:labeled_issue, project: project, labels: [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
+
+        wait_for_vue_resource
+
+        page.within(find('.board', match: :first)) do
+          expect(page.find('.board-header')).to have_content('51')
+          expect(page).to have_selector('.card', count: 20)
+          expect(page).to have_content('Showing 20 of 51 issues')
+
+          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+          expect(page).to have_selector('.card', count: 40)
+          expect(page).to have_content('Showing 40 of 51 issues')
+
+          evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+          expect(page).to have_selector('.card', count: 51)
+          expect(page).to have_content('Showing all issues')
+        end
+      end
+
+      it 'filters by multiple labels' do
+        page.within '.issues-filters' do
+          click_button('Label')
+          wait_for_ajax
+
+          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
+
+        wait_for_vue_resource
+
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..4))
+      end
+
+      it 'filters by no label' do
+        page.within '.issues-filters' do
+          click_button('Label')
+          wait_for_ajax
+
+          page.within '.dropdown-menu-labels' do
+            click_link("No Label")
+            wait_for_vue_resource
+            find('.dropdown-menu-close').click
+          end
+        end
+
+        wait_for_vue_resource
+
+        wait_for_board_cards(1, 5)
+        wait_for_board_cards(2, 0)
+        wait_for_board_cards(3, 0)
+        wait_for_board_cards(4, 1)
+      end
+
+      it 'filters by clicking label button on issue' do
+        page.within(find('.board', match: :first)) do
+          expect(page).to have_selector('.card', count: 6)
+          expect(find('.card', match: :first)).to have_content(bug.title)
+          click_button(bug.title)
+          wait_for_vue_resource
+        end
+
+        wait_for_vue_resource
+
+        wait_for_board_cards(1, 1)
+        wait_for_empty_boards((2..4))
+
+        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
+        page.within(find('.board', match: :first)) 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
+
+  context 'keyboard shortcuts' do
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+    end
+
+    it 'allows user to use keyboard shortcuts' do
+      find('.boards-list').native.send_keys('i')
+      expect(page).to have_content('New Issue')
+    end
+  end
+
+  context 'signed out user' do
+    before do
+      logout
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+    end
+
+    it 'does not show create new list' do
+      expect(page).not_to have_selector('.js-new-board-list')
+    end
+
+    it 'does not allow dragging' do
+      expect(page).not_to have_selector('.user-can-drag')
+    end
+  end
+
+  context 'as guest user' do
+    let(:user_guest) { create(:user) }
+
+    before do
+      project.team << [user_guest, :guest]
+      logout
+      login_as(user_guest)
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+    end
+
+    it 'does not show create new list' do
+      expect(page).not_to have_selector('.js-new-board-list')
+    end
+  end
+
+  def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
+    evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+    Timeout.timeout(Capybara.default_max_wait_time) do
+      loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+    end
+
+    wait_for_vue_resource
+  end
+
+  def wait_for_board_cards(board_number, expected_cards)
+    page.within(find(".board:nth-child(#{board_number})")) do
+      expect(page.find('.board-header')).to have_content(expected_cards.to_s)
+      expect(page).to have_selector('.card', count: expected_cards)
+    end
+  end
+
+  def wait_for_empty_boards(board_numbers)
+    board_numbers.each do |board|
+      wait_for_board_cards(board, 0)
+    end
+  end
+end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a5fc766401f48039714163443aa9fe5448cd02ee
--- /dev/null
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+describe 'Issue Boards shortcut', feature: true, js: true do
+  include WaitForVueResource
+
+  let(:project) { create(:empty_project) }
+
+  before do
+    create(:board, project: project)
+
+    login_as :admin
+
+    visit namespace_project_path(project.namespace, project)
+  end
+
+  it 'takes user to issue board index' do
+    find('body').native.send_keys('gl')
+    expect(page).to have_selector('.boards-list')
+
+    wait_for_vue_resource
+  end
+end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..760a89671239f730a519b1c19de47ffe901ff16d
--- /dev/null
+++ b/spec/features/boards/new_issue_spec.rb
@@ -0,0 +1,96 @@
+require 'rails_helper'
+
+describe 'Issue Boards new issue', feature: true, js: true do
+  include WaitForAjax
+  include WaitForVueResource
+
+  let(:project) { create(:empty_project, :public) }
+  let(:board)   { create(:board, project: project) }
+  let(:user)    { create(:user) }
+
+  context 'authorized user' do
+    before do
+      project.team << [user, :master]
+
+      login_as(user)
+
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.board', count: 3)
+    end
+
+    it 'displays new issue button' do
+      expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
+    end
+
+    it 'does not display new issue button in done list' do
+      page.within('.board:nth-child(3)') do
+        expect(page).not_to have_selector('.board-issue-count-holder .btn')
+      end
+    end
+
+    it 'shows form when clicking button' do
+      page.within(first('.board')) do
+        find('.board-issue-count-holder .btn').click
+
+        expect(page).to have_selector('.board-new-issue-form')
+      end
+    end
+
+    it 'hides form when clicking cancel' do
+      page.within(first('.board')) do
+        find('.board-issue-count-holder .btn').click
+
+        expect(page).to have_selector('.board-new-issue-form')
+
+        click_button 'Cancel'
+
+        expect(page).to have_selector('.board-new-issue-form', visible: false)
+      end
+    end
+
+    it 'creates new issue' do
+      page.within(first('.board')) do
+        find('.board-issue-count-holder .btn').click
+      end
+
+      page.within(first('.board-new-issue-form')) do
+        find('.form-control').set('bug')
+        click_button 'Submit issue'
+      end
+
+      wait_for_vue_resource
+
+      page.within(first('.board .board-issue-count')) do
+        expect(page).to have_content('1')
+      end
+    end
+
+    it 'shows sidebar when creating new issue' do
+      page.within(first('.board')) do
+        find('.board-issue-count-holder .btn').click
+      end
+
+      page.within(first('.board-new-issue-form')) do
+        find('.form-control').set('bug')
+        click_button 'Submit issue'
+      end
+
+      wait_for_vue_resource
+
+      expect(page).to have_selector('.issue-boards-sidebar')
+    end
+  end
+
+  context 'unauthorized user' do
+    before do
+      visit namespace_project_board_path(project.namespace, project, board)
+      wait_for_vue_resource
+    end
+
+    it 'does not display new issue button' do
+      expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
+    end
+  end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f160052a8448624e6e6c134c6162e04c59634905
--- /dev/null
+++ b/spec/features/boards/sidebar_spec.rb
@@ -0,0 +1,312 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+  include WaitForAjax
+  include WaitForVueResource
+
+  let(:project)     { create(:empty_project, :public) }
+  let(:board)       { create(:board, project: project) }
+  let(:user)        { create(:user) }
+  let!(:label)      { create(:label, project: project) }
+  let!(:label2)     { create(:label, project: project) }
+  let!(:milestone)  { create(:milestone, project: project) }
+  let!(:issue2)     { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
+  let!(:issue)      { create(:issue, project: project) }
+
+  before do
+    project.team << [user, :master]
+
+    login_as(user)
+
+    visit namespace_project_board_path(project.namespace, project, board)
+    wait_for_vue_resource
+  end
+
+  it 'shows sidebar when clicking issue' do
+    page.within(first('.board')) do
+      first('.card').click
+    end
+
+    expect(page).to have_selector('.issue-boards-sidebar')
+  end
+
+  it 'closes sidebar when clicking issue' do
+    page.within(first('.board')) do
+      first('.card').click
+    end
+
+    expect(page).to have_selector('.issue-boards-sidebar')
+
+    page.within(first('.board')) do
+      first('.card').click
+    end
+
+    expect(page).not_to have_selector('.issue-boards-sidebar')
+  end
+
+  it 'closes sidebar when clicking close button' do
+    page.within(first('.board')) do
+      first('.card').click
+    end
+
+    expect(page).to have_selector('.issue-boards-sidebar')
+
+    find('.gutter-toggle').click
+
+    expect(page).not_to have_selector('.issue-boards-sidebar')
+  end
+
+  it 'shows issue details when sidebar is open' do
+    page.within(first('.board')) do
+      first('.card').click
+    end
+
+    page.within('.issue-boards-sidebar') do
+      expect(page).to have_content(issue.title)
+      expect(page).to have_content(issue.to_reference)
+    end
+  end
+
+  context 'assignee' do
+    it 'updates the issues assignee' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.assignee') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        page.within('.dropdown-menu-user') do
+          click_link user.name
+
+          wait_for_vue_resource
+        end
+
+        expect(page).to have_content(user.name)
+      end
+
+      page.within(first('.board')) do
+        page.within(first('.card')) do
+          expect(page).to have_selector('.avatar')
+        end
+      end
+    end
+
+    it 'removes the assignee' do
+      page.within(first('.board')) do
+        find('.card:nth-child(2)').click
+      end
+
+      page.within('.assignee') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        page.within('.dropdown-menu-user') do
+          click_link 'Unassigned'
+
+          wait_for_vue_resource
+        end
+
+        expect(page).to have_content('No assignee')
+      end
+
+      page.within(first('.board')) do
+        page.within(find('.card:nth-child(2)')) do
+          expect(page).not_to have_selector('.avatar')
+        end
+      end
+    end
+
+    it 'assignees to current user' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.assignee') do
+        click_link 'assign yourself'
+
+        wait_for_vue_resource
+
+        expect(page).to have_content(user.name)
+      end
+
+      page.within(first('.board')) do
+        page.within(first('.card')) do
+          expect(page).to have_selector('.avatar')
+        end
+      end
+    end
+  end
+
+  context 'milestone' do
+    it 'adds a milestone' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.milestone') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        click_link milestone.title
+
+        wait_for_vue_resource
+
+        page.within('.value') do
+          expect(page).to have_content(milestone.title)
+        end
+      end
+    end
+
+    it 'removes a milestone' do
+      page.within(first('.board')) do
+        find('.card:nth-child(2)').click
+      end
+
+      page.within('.milestone') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        click_link "No Milestone"
+
+        wait_for_vue_resource
+
+        page.within('.value') do
+          expect(page).not_to have_content(milestone.title)
+        end
+      end
+    end
+  end
+
+  context 'due date' do
+    it 'updates due date' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.due_date') do
+        click_link 'Edit'
+
+        click_link Date.today.day
+
+        wait_for_vue_resource
+
+        expect(page).to have_content(Date.today.to_s(:medium))
+      end
+    end
+  end
+
+  context 'labels' do
+    it 'adds a single label' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.labels') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        click_link label.title
+
+        wait_for_vue_resource
+
+        find('.dropdown-menu-close-icon').click
+
+        page.within('.value') do
+          expect(page).to have_selector('.label', count: 1)
+          expect(page).to have_content(label.title)
+        end
+      end
+
+      page.within(first('.board')) do
+        page.within(first('.card')) do
+          expect(page).to have_selector('.label', count: 1)
+          expect(page).to have_content(label.title)
+        end
+      end
+    end
+
+    it 'adds a multiple labels' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.labels') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        click_link label.title
+        click_link label2.title
+
+        wait_for_vue_resource
+
+        find('.dropdown-menu-close-icon').click
+
+        page.within('.value') do
+          expect(page).to have_selector('.label', count: 2)
+          expect(page).to have_content(label.title)
+          expect(page).to have_content(label2.title)
+        end
+      end
+
+      page.within(first('.board')) do
+        page.within(first('.card')) do
+          expect(page).to have_selector('.label', count: 2)
+          expect(page).to have_content(label.title)
+          expect(page).to have_content(label2.title)
+        end
+      end
+    end
+
+    it 'removes a label' do
+      page.within(first('.board')) do
+        find('.card:nth-child(2)').click
+      end
+
+      page.within('.labels') do
+        click_link 'Edit'
+
+        wait_for_ajax
+
+        click_link label.title
+
+        wait_for_vue_resource
+
+        find('.dropdown-menu-close-icon').click
+
+        page.within('.value') do
+          expect(page).to have_selector('.label', count: 0)
+          expect(page).not_to have_content(label.title)
+        end
+      end
+
+      page.within(first('.board')) do
+        page.within(find('.card:nth-child(2)')) do
+          expect(page).not_to have_selector('.label', count: 1)
+          expect(page).not_to have_content(label.title)
+        end
+      end
+    end
+  end
+
+  context 'subscription' do
+    it 'changes issue subscription' do
+      page.within(first('.board')) do
+        first('.card').click
+      end
+
+      page.within('.subscription') do
+        click_button 'Subscribe'
+
+        expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+      end
+    end
+  end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7fa0c95cae295153fea15a62b12d18e2387193bf
--- /dev/null
+++ b/spec/features/calendar_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+feature 'Contributions Calendar', js: true, feature: true do
+  include WaitForAjax
+
+  let(:contributed_project) { create(:project, :public) }
+
+  # Ex/ Sunday Jan 1, 2016
+  date_format = '%A %b %-d, %Y'
+
+  issue_title = 'Bug in old browser'
+  issue_params = { title: issue_title }
+
+  def get_cell_color_selector(contributions)
+    contribution_cell = '.user-contrib-cell'
+    activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77']
+    activity_colors_index = 0
+
+    if contributions > 0 && contributions < 10
+      activity_colors_index = 1
+    elsif contributions >= 10 && contributions < 20
+      activity_colors_index = 2
+    elsif contributions >= 20 && contributions < 30
+      activity_colors_index = 3
+    elsif contributions >= 30
+      activity_colors_index = 4
+    end
+
+    "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']"
+  end
+
+  def get_cell_date_selector(contributions, date)
+    contribution_text = 'No contributions'
+
+    if contributions === 1
+      contribution_text = '1 contribution'
+    elsif contributions > 1
+      contribution_text = "#{contributions} contributions"
+    end
+
+    "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']"
+  end
+
+  def push_code_contribution
+    push_params = {
+      project: contributed_project,
+      action: Event::PUSHED,
+      author_id: @user.id,
+      data: { commit_count: 3 }
+    }
+
+    Event.create(push_params)
+  end
+
+  before do
+    login_as :user
+    visit @user.username
+    wait_for_ajax
+  end
+
+  it 'displays calendar', js: true do
+    expect(page).to have_css('.js-contrib-calendar')
+  end
+
+  describe '1 calendar activity' do
+    before do
+      Issues::CreateService.new(contributed_project, @user, issue_params).execute
+      visit @user.username
+      wait_for_ajax
+    end
+
+    it 'displays calendar activity log', js: true do
+      expect(find('.content_list .event-note')).to have_content issue_title
+    end
+
+    it 'displays calendar activity square color for 1 contribution', js: true do
+      expect(page).to have_selector(get_cell_color_selector(1), count: 1)
+    end
+
+    it 'displays calendar activity square on the correct date', js: true do
+      today = Date.today.strftime(date_format)
+      expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+    end
+  end
+
+  describe '10 calendar activities' do
+    before do
+      (0..9).each do |i|
+        push_code_contribution()
+      end
+
+      visit @user.username
+      wait_for_ajax
+    end
+
+    it 'displays calendar activity square color for 10 contributions', js: true do
+      expect(page).to have_selector(get_cell_color_selector(10), count: 1)
+    end
+
+    it 'displays calendar activity square on the correct date', js: true do
+      today = Date.today.strftime(date_format)
+      expect(page).to have_selector(get_cell_date_selector(10, today), count: 1)
+    end
+  end
+
+  describe 'calendar activity on two days' do
+    before do
+      push_code_contribution()
+
+      Timecop.freeze(Date.yesterday)
+      Issues::CreateService.new(contributed_project, @user, issue_params).execute
+      Timecop.return
+
+      visit @user.username
+      wait_for_ajax
+    end
+
+    it 'displays calendar activity squares for both days', js: true do
+      expect(page).to have_selector(get_cell_color_selector(1), count: 2)
+    end
+
+    it 'displays calendar activity square for yesterday', js: true do
+      yesterday = Date.yesterday.strftime(date_format)
+      expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+    end
+
+    it 'displays calendar activity square for today', js: true do
+      today = Date.today.strftime(date_format)
+      expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+    end
+  end
+end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5910803df51f4f2c6541da76827fbe2f4cb8962f..44646ffc602090da92530aa44b55d7996611c75a 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -12,11 +12,15 @@ describe 'Commits' do
     end
 
     let!(:pipeline) do
-      FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha
+      create(:ci_pipeline,
+             project: project,
+             ref: project.default_branch,
+             sha: project.commit.sha,
+             status: :success)
     end
 
     context 'commit status is Generic Commit Status' do
-      let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
+      let!(:status) { create(:generic_commit_status, pipeline: pipeline) }
 
       before do
         project.team << [@user, :reporter]
@@ -39,7 +43,7 @@ describe 'Commits' do
     end
 
     context 'commit status is Ci Build' do
-      let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline }
+      let!(:build) { create(:ci_build, pipeline: pipeline) }
       let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
 
       context 'when logged as developer' do
@@ -48,13 +52,22 @@ describe 'Commits' do
         end
 
         describe 'Project commits' do
+          let!(:pipeline_from_other_branch) do
+            create(:ci_pipeline,
+                   project: project,
+                   ref: 'fix',
+                   sha: project.commit.sha,
+                   status: :failed)
+          end
+
           before do
             visit namespace_project_commits_path(project.namespace, project, :master)
           end
 
-          it 'shows build status' do
+          it 'shows correct build status from default branch' do
             page.within("//li[@id='commit-#{pipeline.short_sha}']") do
-              expect(page).to have_css(".ci-status-link")
+              expect(page).to have_css('.ci-status-link')
+              expect(page).to have_css('.ci-status-icon-success')
             end
           end
         end
@@ -64,9 +77,11 @@ describe 'Commits' do
             visit ci_status_path(pipeline)
           end
 
-          it { expect(page).to have_content pipeline.sha[0..7] }
-          it { expect(page).to have_content pipeline.git_commit_message }
-          it { expect(page).to have_content pipeline.git_author_name }
+          it 'shows pipeline`s data' do
+            expect(page).to have_content pipeline.sha[0..7]
+            expect(page).to have_content pipeline.git_commit_message
+            expect(page).to have_content pipeline.git_author_name
+          end
         end
 
         context 'Download artifacts' do
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index ca7f73e24cc864d9290cc2407c259b59d4ef4eb8..43eb4000e5866e7617ae27cb508f6730cb93464e 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/compare_spec.rb
@@ -12,15 +12,16 @@ describe "Compare", js: true do
 
   describe "branches" do
     it "pre-populates fields" do
-      expect(page.find_field("from").value).to eq("master")
+      expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
+      expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
     end
 
     it "compares branches" do
-      fill_in "from", with: "fea"
-      find("#from").click
+      select_using_dropdown "from", "feature"
+      expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
 
-      click_link "feature"
-      expect(page.find_field("from").value).to eq("feature")
+      select_using_dropdown "to", "binary-encoding"
+      expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
 
       click_button "Compare"
       expect(page).to have_content "Commits"
@@ -29,14 +30,21 @@ describe "Compare", js: true do
 
   describe "tags" do
     it "compares tags" do
-      fill_in "from", with: "v1.0"
-      find("#from").click
+      select_using_dropdown "from", "v1.0.0"
+      expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
 
-      click_link "v1.0.0"
-      expect(page.find_field("from").value).to eq("v1.0.0")
+      select_using_dropdown "to", "v1.1.0"
+      expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
 
       click_button "Compare"
       expect(page).to have_content "Commits"
     end
   end
+
+  def select_using_dropdown(dropdown_type, selection)
+    dropdown = find(".js-compare-#{dropdown_type}-dropdown")
+    dropdown.find(".compare-dropdown-toggle").click
+    dropdown.fill_in("Filter by Git revision", with: selection)
+    find_link(selection, visible: true).click
+  end
 end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba77093a6d4b56f3f4d322dfba1be4de4fa77085
--- /dev/null
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Project member activity', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user)            { create(:user) }
+  let(:project)         { create(:empty_project, :public, name: 'x', namespace: user.namespace) }
+
+  before do
+    project.team << [user, :master]
+  end
+
+  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)
+    wait_for_ajax
+  end
+
+  subject { page.find(".event-title").text }
+
+  context 'when a user joins the project' do
+    before { visit_activities_and_wait_with_event(Event::JOINED) }
+
+    it { is_expected.to eq("#{user.name} joined project") }
+  end
+
+  context 'when a user leaves the project' do
+    before { visit_activities_and_wait_with_event(Event::LEFT) }
+
+    it { is_expected.to eq("#{user.name} left project") }
+  end
+
+  context 'when a users membership expires for the project' do
+    before { visit_activities_and_wait_with_event(Event::EXPIRED) }
+
+    it "presents the correct message" do
+      message = "#{user.name} removed due to membership expiration from project"
+      is_expected.to eq(message)
+    end
+  end
+end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62937688c2273aaed5099b5cf7735a60bf29f191
--- /dev/null
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'Dashboard snippets', feature: true do
+  context 'when the project has snippets' do
+    let(:project) { create(:empty_project, :public) }
+    let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+    before do
+      allow(Snippet).to receive(:default_per_page).and_return(1)
+      login_as(project.owner)
+      visit dashboard_snippets_path
+    end
+
+    it_behaves_like 'paginated snippets'
+  end
+end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 3fb1cb37544717b3a1c3747a546c6d056f52979b..b898f9bc64fb3ca7c065b7df443c1479262751b8 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
 
       click_link 'No Milestone'
 
+      expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
       expect(page).to have_selector('.issue', count: 1)
     end
 
@@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
 
       click_link 'Any Milestone'
 
+      expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
       expect(page).to have_selector('.issue', count: 2)
     end
 
@@ -39,8 +41,25 @@ describe "Dashboard Issues filtering", feature: true, js: true do
         click_link milestone.title
       end
 
+      expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
       expect(page).to have_selector('.issue', count: 1)
     end
+
+    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)
+      auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+      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' => [''])
+      expect(params).to include('assignee_id' => [user.id.to_s])
+      expect(auto_discovery_params).to include('private_token' => [user.private_token])
+      expect(auto_discovery_params).to include('milestone_title' => [''])
+      expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+    end
   end
 
   def show_milestone_dropdown
@@ -48,7 +67,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
     expect(page).to have_selector('.dropdown-content', visible: true)
   end
 
-  def visit_issues
-    visit issues_dashboard_path
+  def visit_issues(*args)
+    visit issues_dashboard_path(*args)
   end
 end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index fcd41b38413112748114d42fd628de10a680be05..b565586ee1454c4ea9c1c980f5e1a636ef00850c 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -19,10 +19,22 @@ feature 'Environments', feature: true do
       visit namespace_project_environments_path(project.namespace, project)
     end
 
+    context 'shows two tabs' do
+      scenario 'shows "Available" and "Stopped" tab with links' do
+        expect(page).to have_link('Available')
+        expect(page).to have_link('Stopped')
+      end
+    end
+
     context 'without environments' do
       scenario 'does show no environments' do
         expect(page).to have_content('You don\'t have any environments right now.')
       end
+
+      scenario 'does show 0 as counter for environments in both tabs' do
+        expect(page.find('.js-available-environments-count').text).to eq('0')
+        expect(page.find('.js-stopped-environments-count').text).to eq('0')
+      end
     end
 
     context 'with environments' do
@@ -32,6 +44,11 @@ feature 'Environments', feature: true do
         expect(page).to have_link(environment.name)
       end
 
+      scenario 'does show number of available and stopped environments' do
+        expect(page.find('.js-available-environments-count').text).to eq('1')
+        expect(page.find('.js-stopped-environments-count').text).to eq('0')
+      end
+
       context 'without deployments' do
         scenario 'does show no deployments' do
           expect(page).to have_content('No deployments yet')
@@ -45,6 +62,10 @@ feature 'Environments', feature: true do
           expect(page).to have_link(deployment.short_sha)
         end
 
+        scenario 'does show deployment internal id' do
+          expect(page).to have_content(deployment.iid)
+        end
+
         context 'with build and manual actions' do
           given(:pipeline) { create(:ci_pipeline, project: project) }
           given(:build) { create(:ci_build, pipeline: pipeline) }
@@ -61,6 +82,51 @@ feature 'Environments', feature: true do
             expect(page).to have_content(manual.name)
             expect(manual.reload).to be_pending
           end
+
+          scenario 'does show build name and id' do
+            expect(page).to have_link("#{build.name} (##{build.id})")
+          end
+
+          scenario 'does not show stop button' do
+            expect(page).not_to have_selector('.stop-env-link')
+          end
+
+          scenario 'does not show external link button' do
+            expect(page).not_to have_css('external-url')
+          end
+
+          context 'with external_url' do
+            given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+            given(:build) { create(:ci_build, pipeline: pipeline) }
+            given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+            scenario 'does show an external link button' do
+              expect(page).to have_link(nil, href: environment.external_url)
+            end
+          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')
+            end
+
+            scenario 'starts build when stop button clicked' do
+              first('.stop-env-link').click
+
+              expect(page).to have_content('close_app')
+            end
+
+            context 'for reporter' do
+              let(:role) { :reporter }
+
+              scenario 'does not show stop button' do
+                expect(page).not_to have_selector('.stop-env-link')
+              end
+            end
+          end
         end
       end
     end
@@ -109,6 +175,10 @@ feature 'Environments', feature: true do
           expect(page).to have_link('Re-deploy')
         end
 
+        scenario 'does not show stop button' do
+          expect(page).not_to have_link('Stop')
+        end
+
         context 'with manual action' do
           given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
 
@@ -122,6 +192,39 @@ feature 'Environments', feature: true do
             expect(page).to have_content(manual.name)
             expect(manual.reload).to be_pending
           end
+
+          context 'with external_url' do
+            given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+            given(:build) { create(:ci_build, pipeline: pipeline) }
+            given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+            scenario 'does show an external link button' do
+              expect(page).to have_link(nil, href: environment.external_url)
+            end
+          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_link('Stop')
+            end
+
+            scenario 'does allow to stop environment' do
+              click_link('Stop')
+
+              expect(page).to have_content('close_app')
+            end
+
+            context 'for reporter' do
+              let(:role) { :reporter }
+
+              scenario 'does not show stop button' do
+                expect(page).not_to have_link('Stop')
+              end
+            end
+          end
         end
       end
     end
@@ -150,7 +253,7 @@ feature 'Environments', feature: true do
 
       context 'for invalid name' do
         before do
-          fill_in('Name', with: 'name with spaces')
+          fill_in('Name', with: 'name,with,commas')
           click_on 'Save'
         end
 
@@ -168,29 +271,4 @@ feature 'Environments', feature: true do
       end
     end
   end
-
-  describe 'when deleting existing environment' do
-    given(:environment) { create(:environment, project: project) }
-
-    before do
-      visit namespace_project_environment_path(project.namespace, project, environment)
-    end
-
-    context 'when logged as master' do
-      given(:role) { :master }
-
-      scenario 'does delete environment' do
-        click_link 'Destroy'
-        expect(page).not_to have_link(environment.name)
-      end
-    end
-
-    context 'when logged as developer' do
-      given(:role) { :developer }
-
-      scenario 'does not have a Destroy link' do
-        expect(page).not_to have_link('Destroy')
-      end
-    end
-  end
 end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 688f68d3cff30c56dd75ed2919af35e398b27ed0..6c938bdead8fc54aa7068cd884807a305d356729 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
 
     context 'expanding a diff for a renamed file' do
       before do
-        large_diff_renamed.find('.nothing-here-block').click
+        large_diff_renamed.find('.click-to-expand').click
         wait_for_ajax
       end
 
@@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
 
     context 'expanding a large diff' do
       before do
-        click_link('large_diff.md')
+        # Wait for diffs
+        find('.file-title', match: :first)
+        # Click `large_diff.md` title
+        all('.file-title')[1].click
         wait_for_ajax
       end
 
@@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
 
           context 'expanding the diff' do
             before do
-              click_link('large_diff.md')
+              # Wait for diffs
+              find('.file-title', match: :first)
+              # Click `large_diff.md` title
+              all('.file-title')[1].click
               wait_for_ajax
             end
 
@@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
     end
 
     context 'collapsing an expanded diff' do
-      before { click_link('small_diff.md') }
+      before do
+        # Wait for diffs
+        find('.file-title', match: :first)
+        # Click `small_diff.md` title
+        all('.file-title')[3].click
+      end
 
       it 'hides the diff content' do
         expect(small_diff).not_to have_selector('.code')
@@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
       end
 
       context 're-expanding the same diff' do
-        before { click_link('small_diff.md') }
+        before do
+          # Wait for diffs
+          find('.file-title', match: :first)
+          # Click `small_diff.md` title
+          all('.file-title')[3].click
+        end
 
         it 'shows the diff content' do
           expect(small_diff).to have_selector('.code')
@@ -211,6 +227,13 @@ feature 'Expand and collapse diffs', js: true, feature: true do
   context 'expanding all diffs' do
     before do
       click_link('Expand all')
+
+      # Wait for elements to appear to ensure full page reload
+      expect(page).to have_content('This diff was suppressed by a .gitattributes entry')
+      expect(page).to have_content('This diff could not be displayed because it is too large.')
+      expect(page).to have_content('too_large_image.jpg')
+      find('.note-textarea')
+
       wait_for_ajax
       execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
     end
@@ -224,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
     end
 
     context 'collapsing an expanded diff' do
-      before { click_link('small_diff.md') }
+      before do
+        # Wait for diffs
+        find('.file-title', match: :first)
+        # Click `small_diff.md` title
+        all('.file-title')[3].click
+      end
 
       it 'hides the diff content' do
         expect(small_diff).not_to have_selector('.code')
@@ -232,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
       end
 
       context 're-expanding the same diff' do
-        before { click_link('small_diff.md') }
+        before do
+          # Wait for diffs
+          find('.file-title', match: :first)
+          # Click `small_diff.md` title
+          all('.file-title')[3].click
+        end
 
         it 'shows the diff content' do
           expect(small_diff).to have_selector('.code')
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6409e00f22fb2b4180d47480faa754f6c2661ee
--- /dev/null
+++ b/spec/features/global_search_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Global search', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, namespace: user.namespace) }
+
+  before do
+    project.team << [user, :master]
+    login_with(user)
+  end
+
+  describe 'I search through the issues and I see pagination' do
+    before do
+      allow_any_instance_of(Gitlab::SearchResults).to receive(:per_page).and_return(1)
+      create_list(:issue, 2, project: project, title: 'initial')
+    end
+
+    it "has a pagination" do
+      visit dashboard_projects_path
+
+      fill_in "search", with: "initial"
+      click_button "Go"
+
+      select_filter("Issues")
+      expect(page).to have_selector('.gl-pagination .page', count: 2)
+    end
+  end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..476eca17a9d8e36c3ee7bad6679ebf98febc434f
--- /dev/null
+++ b/spec/features/groups/issues_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Group issues page', feature: true do
+  let(:path) { issues_group_path(group) }
+  let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
+
+  include_examples 'project features apply to issuables', Issue
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index 10d3713f19f7264582226365d3f98888340df6fa..d811b05b0c3023e4cbb39bf47fb590cfb6e27c55 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -41,7 +41,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do
 
   def expect_visible_access_request(group, user)
     expect(group.requesters.exists?(user_id: user)).to be_truthy
-    expect(page).to have_content "#{group.name} access requests 1"
+    expect(page).to have_content "Users requesting access to #{group.name} 1"
     expect(page).to have_content user.name
   end
 end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a2791b5754407a3cfee9deec2c54b0c80cb58cd7
--- /dev/null
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Group merge requests page', feature: true do
+  let(:path) { merge_requests_group_path(group) }
+  let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")}
+
+  include_examples 'project features apply to issuables', MergeRequest
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 2d8b59472e8733abf76c9c08f1b4323c587c750b..13bfe90302cb0dffa0a5ca989867be56278ae73b 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -5,43 +5,105 @@ feature 'Group', feature: true do
     login_as(:admin)
   end
 
-  describe 'creating a group with space in group path' do
-    it 'renders new group form with validation errors' do
-      visit new_group_path
-      fill_in 'Group path', with: 'space group'
+  matcher :have_namespace_error_message do
+    match do |page|
+      page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
+    end
+  end
+
+  describe 'create a group' do
+    before { visit new_group_path }
+
+    describe 'with space in group path' do
+      it 'renders new group form with validation errors' do
+        fill_in 'Group path', with: 'space group'
+        click_button 'Create group'
+
+        expect(current_path).to eq(groups_path)
+        expect(page).to have_namespace_error_message
+      end
+    end
+
+    describe 'with .atom at end of group path' do
+      it 'renders new group form with validation errors' do
+        fill_in 'Group path', with: 'atom_group.atom'
+        click_button 'Create group'
+
+        expect(current_path).to eq(groups_path)
+        expect(page).to have_namespace_error_message
+      end
+    end
+
+    describe 'with .git at end of group path' do
+      it 'renders new group form with validation errors' do
+        fill_in 'Group path', with: 'git_group.git'
+        click_button 'Create group'
+
+        expect(current_path).to eq(groups_path)
+        expect(page).to have_namespace_error_message
+      end
+    end
+  end
+
+  describe 'group edit' do
+    let(:group) { create(:group) }
+    let(:path)  { edit_group_path(group) }
+    let(:new_name) { 'new-name' }
+
+    before { visit path }
 
-      click_button 'Create group'
+    it 'saves new settings' do
+      fill_in 'group_name', with: new_name
+      click_button 'Save group'
 
-      expect(current_path).to eq(groups_path)
-      expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.")
+      expect(page).to have_content 'successfully updated'
+      expect(find('#group_name').value).to eq(new_name)
+
+      page.within ".navbar-gitlab" do
+        expect(page).to have_content new_name
+      end
+    end
+
+    it 'removes group' do
+      click_link 'Remove Group'
+
+      expect(page).to have_content "scheduled for deletion"
     end
   end
 
-  describe 'description' do
+  describe 'group page with markdown description' do
     let(:group) { create(:group) }
     let(:path)  { group_path(group) }
 
     it 'parses Markdown' do
       group.update_attribute(:description, 'This is **my** group')
+
       visit path
+
       expect(page).to have_css('.description > p > strong')
     end
 
     it 'passes through html-pipeline' do
       group.update_attribute(:description, 'This group is the :poop:')
+
       visit path
+
       expect(page).to have_css('.description > p > img')
     end
 
     it 'sanitizes unwanted tags' do
       group.update_attribute(:description, '# Group Description')
+
       visit path
+
       expect(page).not_to have_css('.description h1')
     end
 
     it 'permits `rel` attribute on links' do
       group.update_attribute(:description, 'https://google.com/')
+
       visit path
+
       expect(page).to have_css('.description a[rel]')
     end
   end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 9114f751b5518eb95fed9855969f53cb7870ebc3..9a2b879e789a97f19bf2b2ef240016428be6b774 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -149,6 +149,30 @@ describe 'Projects > Issuables > Default sort order', feature: true do
         expect(last_issue).to include(first_created_issuable.title)
       end
     end
+
+    context 'when the sort in the URL is id_desc' do
+      let(:issuable_type) { :issue }
+
+      before { visit_issues(project, sort: 'id_desc') }
+
+      it 'shows the sort order as last created' do
+        expect(find('.issues-other-filters')).to have_content('Last created')
+        expect(first_issue).to include(last_created_issuable.title)
+        expect(last_issue).to include(first_created_issuable.title)
+      end
+    end
+
+    context 'when the sort in the URL is id_asc' do
+      let(:issuable_type) { :issue }
+
+      before { visit_issues(project, sort: 'id_asc') }
+
+      it 'shows the sort order as oldest created' do
+        expect(find('.issues-other-filters')).to have_content('Oldest created')
+        expect(first_issue).to include(first_created_issuable.title)
+        expect(last_issue).to include(last_created_issuable.title)
+      end
+    end
   end
 
   def selected_sort_order
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 6eb04cf74c523db554b8574e3eaab06ca8eb85de..ef00f2099984f866bc7650ec0b537399cc30c6b3 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -1,6 +1,8 @@
 require 'rails_helper'
 
 describe 'Awards Emoji', feature: true do
+  include WaitForAjax
+
   let!(:project)   { create(:project) }
   let!(:user)      { create(:user) }
 
@@ -12,25 +14,26 @@ describe 'Awards Emoji', feature: true do
   describe 'Click award emoji from issue#show' do
     let!(:issue) do
       create(:issue,
-             author: @user,
              assignee: @user,
              project: project)
     end
 
+    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
-      sleep 2
+      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
-        sleep 2
+        wait_for_ajax
         expect(thumbsup_emoji).to have_text("1")
       end
 
@@ -42,7 +45,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
-        sleep 2
+        wait_for_ajax
         expect(thumbsdown_emoji).to have_text("1")
       end
 
@@ -50,13 +53,45 @@ describe 'Awards Emoji', feature: true do
         expect(thumbsup_emoji).to have_text("0")
       end
     end
+
+    it 'toggles the smiley emoji on a note', js: true do
+      toggle_smiley_emoji(true)
+
+      within('.note-awards') do
+        expect(find(emoji_counter)).to have_text("1")
+      end
+
+      toggle_smiley_emoji(false)
+
+      within('.note-awards') do
+        expect(page).not_to have_selector(emoji_counter)
+      end
+    end
   end
 
   def thumbsup_emoji
-    page.all('span.js-counter').first
+    page.all(emoji_counter).first
   end
 
   def thumbsdown_emoji
-    page.all('span.js-counter').last
+    page.all(emoji_counter).last
+  end
+
+  def emoji_counter
+    'span.js-counter'
+  end
+
+  def toggle_smiley_emoji(status)
+    within('.note') do
+      find('.note-emoji-button').click
+    end
+
+    unless status
+      first('[data-emoji="smiley"]').click
+    else
+      find('[data-emoji="smiley"]').click
+    end
+
+    wait_for_ajax
   end
 end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 908b18e5339cdf55cc8f5b2e0fdab047a1c3753c..0253629f753fb03602b897f5af04af09f74913e8 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -1,10 +1,10 @@
 require 'rails_helper'
 
-feature 'Issue filtering by Labels', feature: true do
+feature 'Issue filtering by Labels', feature: true, js: true do
   include WaitForAjax
 
   let(:project) { create(:project, :public) }
-  let!(:user)   { create(:user)}
+  let!(:user)   { create(:user) }
   let!(:label)  { create(:label, project: project) }
 
   before do
@@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do
     visit namespace_project_issues_path(project.namespace, project)
   end
 
-  context 'filter by label bug', js: true do
+  context 'filter by label bug' do
     before do
-      page.find('.js-label-select').click
-      wait_for_ajax
-      execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
-      page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-      wait_for_ajax
+      select_labels('bug')
     end
 
-    it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
+    it 'apply the filter' do
       expect(page).to have_content "Bugfix1"
       expect(page).to have_content "Bugfix2"
-    end
-
-    it 'does not show "Feature1" in issues list' do
       expect(page).not_to have_content "Feature1"
-    end
-
-    it 'shows label "bug" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "bug"
-    end
-
-    it 'does not show label "feature" and "enhancement" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "feature"
       expect(find('.filtered-labels')).not_to have_content "enhancement"
-    end
 
-    it 'removes label "bug"' do
       find('.js-label-filter-remove').click
       wait_for_ajax
       expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
     end
   end
 
-  context 'filter by label feature', js: true do
+  context 'filter by label feature' do
     before do
-      page.find('.js-label-select').click
-      wait_for_ajax
-      execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
-      page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-      wait_for_ajax
+      select_labels('feature')
     end
 
-    it 'shows issue "Feature1" in issues list' do
+    it 'applies the filter' do
       expect(page).to have_content "Feature1"
-    end
-
-    it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
       expect(page).not_to have_content "Bugfix2"
       expect(page).not_to have_content "Bugfix1"
-    end
-
-    it 'shows label "feature" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "feature"
-    end
-
-    it 'does not show label "bug" and "enhancement" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "bug"
       expect(find('.filtered-labels')).not_to have_content "enhancement"
     end
   end
 
-  context 'filter by label enhancement', js: true do
+  context 'filter by label enhancement' do
     before do
-      page.find('.js-label-select').click
-      wait_for_ajax
-      execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
-      page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-      wait_for_ajax
+      select_labels('enhancement')
     end
 
-    it 'shows issue "Bugfix2" in issues list' do
+    it 'applies the filter' do
       expect(page).to have_content "Bugfix2"
-    end
-
-    it 'does not show "Feature1" and "Bugfix1" in issues list' do
       expect(page).not_to have_content "Feature1"
       expect(page).not_to have_content "Bugfix1"
-    end
-
-    it 'shows label "enhancement" in filtered-labels' do
       expect(find('.filtered-labels')).to have_content "enhancement"
-    end
-
-    it 'does not show label "feature" and "bug" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "bug"
       expect(find('.filtered-labels')).not_to have_content "feature"
     end
   end
 
-  context 'filter by label enhancement or feature', js: true do
+  context 'filter by label enhancement and bug in issues list' do
     before do
-      page.find('.js-label-select').click
-      wait_for_ajax
-      execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
-      execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
-      page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-      wait_for_ajax
+      select_labels('bug', 'enhancement')
     end
 
-    it 'does not show "Bugfix1" or "Feature1" in issues list' do
-      expect(page).not_to have_content "Bugfix1"
+    it 'applies the filters' do
+      expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+      expect(page).to have_content "Bugfix2"
       expect(page).not_to have_content "Feature1"
-    end
-
-    it 'shows label "enhancement" and "feature" in filtered-labels' do
+      expect(find('.filtered-labels')).to have_content "bug"
       expect(find('.filtered-labels')).to have_content "enhancement"
-      expect(find('.filtered-labels')).to have_content "feature"
-    end
-
-    it 'does not show label "bug" in filtered-labels' do
-      expect(find('.filtered-labels')).not_to have_content "bug"
-    end
+      expect(find('.filtered-labels')).not_to have_content "feature"
 
-    it 'removes label "enhancement"' do
       find('.js-label-filter-remove', match: :first).click
       wait_for_ajax
-      expect(find('.filtered-labels')).to have_no_content "enhancement"
-    end
-  end
-
-  context 'filter by label enhancement and bug in issues list', js: true do
-    before do
-      page.find('.js-label-select').click
-      wait_for_ajax
-      execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
-      execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
-      page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-      wait_for_ajax
-    end
 
-    it 'shows issue "Bugfix2" in issues list' do
       expect(page).to have_content "Bugfix2"
-    end
-
-    it 'does not show "Feature1"' do
       expect(page).not_to have_content "Feature1"
-    end
-
-    it 'shows label "bug" and "enhancement" in filtered-labels' do
-      expect(find('.filtered-labels')).to have_content "bug"
+      expect(page).not_to have_content "Bugfix1"
+      expect(find('.filtered-labels')).not_to have_content "bug"
       expect(find('.filtered-labels')).to have_content "enhancement"
-    end
-
-    it 'does not show label "feature" in filtered-labels' do
       expect(find('.filtered-labels')).not_to have_content "feature"
     end
   end
 
-  context 'remove filtered labels', js: true do
+  context 'remove filtered labels' do
     before do
       page.within '.labels-filter' do
         click_button 'Label'
@@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do
     end
   end
 
-  context 'dropdown filtering', js: true do
+  context 'dropdown filtering' do
     it 'filters by label name' do
       page.within '.labels-filter' do
         click_button 'Label'
@@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do
       end
     end
   end
+
+  def select_labels(*labels)
+    page.find('.js-label-select').click
+    wait_for_ajax
+    labels.each do |label|
+      execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
+    end
+    page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+    wait_for_ajax
+  end
 end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 485dc5600616d6cf5fe24cff40fe4f129744862d..9dfa5d1de1991eb543acc69bbd3023dd4ce14d39 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do
     visit_issues(project)
     filter_by_milestone(Milestone::None.title)
 
+    expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
     expect(page).to have_css('.issue', count: 1)
   end
 
@@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
       visit_issues(project)
       filter_by_milestone(Milestone::Upcoming.title)
 
+      expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
       expect(page).to have_css('.issue', count: 0)
     end
 
@@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do
       visit_issues(project)
       filter_by_milestone(Milestone::Upcoming.title)
 
+      expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
       expect(page).to have_css('.issue', count: 1)
     end
 
@@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do
       visit_issues(project)
       filter_by_milestone(Milestone::Upcoming.title)
 
+      expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
       expect(page).to have_css('.issue', count: 0)
     end
   end
@@ -55,9 +59,27 @@ feature 'Issue filtering by Milestone', feature: true do
     visit_issues(project)
     filter_by_milestone(milestone.title)
 
+    expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
     expect(page).to have_css('.issue', count: 1)
   end
 
+  context 'when milestone has single quotes in title' do
+    background do
+      milestone.update(name: "rock 'n' roll")
+    end
+
+    scenario 'filters by a specific Milestone', js: true do
+      create(:issue, project: project, milestone: milestone)
+      create(:issue, project: project)
+
+      visit_issues(project)
+      filter_by_milestone(milestone.title)
+
+      expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
+      expect(page).to have_css('.issue', count: 1)
+    end
+  end
+
   def visit_issues(project)
     visit namespace_project_issues_path(project.namespace, project)
   end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index e262f2858680e99e3997abd84fad64c6a5865e72..2798db92f0f7b769045a08854b43284ec3f5f29e 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -4,17 +4,20 @@ describe 'Filter issues', feature: true do
   include WaitForAjax
 
   let!(:project)   { create(:project) }
+  let!(:group)     { create(:group) }
   let!(:user)      { create(:user)}
   let!(:milestone) { create(:milestone, project: project) }
   let!(:label)     { create(:label, project: project) }
-  let!(:issue1)    { create(:issue, project: project) }
+  let!(:wontfix)   { create(:label, project: project, title: "Won't fix") }
 
   before do
     project.team << [user, :master]
+    group.add_developer(user)
     login_as(user)
+    create(:issue, project: project)
   end
 
-  describe 'Filter issues for assignee from issues#index' do
+  describe 'for assignee from issues#index' do
     before do
       visit namespace_project_issues_path(project.namespace, project)
 
@@ -44,7 +47,7 @@ describe 'Filter issues', feature: true do
     end
   end
 
-  describe 'Filter issues for milestone from issues#index' do
+  describe 'for milestone from issues#index' do
     before do
       visit namespace_project_issues_path(project.namespace, project)
 
@@ -74,7 +77,7 @@ describe 'Filter issues', feature: true do
     end
   end
 
-  describe 'Filter issues for label from issues#index', js: true do
+  describe 'for label from issues#index', js: true do
     before do
       visit namespace_project_issues_path(project.namespace, project)
       find('.js-label-select').click
@@ -95,21 +98,65 @@ describe 'Filter issues', feature: true do
       wait_for_ajax
 
       page.within '.labels-filter' do
-        expect(page).to have_content 'No Label'
+        expect(page).to have_content 'Labels'
       end
-      expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
+      expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
     end
 
-    it 'filters by no label' do
+    it 'filters by a label' do
       find('.dropdown-menu-labels a', text: label.title).click
       page.within '.labels-filter' do
         expect(page).to have_content label.title
       end
       expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
     end
+
+    it "filters by `won't fix` and another label" do
+      page.within '.labels-filter' do
+        click_link wontfix.title
+        expect(page).to have_content wontfix.title
+        click_link label.title
+      end
+
+      expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
+    end
+
+    it "filters by `won't fix` label followed by another label after page load" do
+      page.within '.labels-filter' do
+        click_link wontfix.title
+        expect(page).to have_content wontfix.title
+      end
+
+      find('body').click
+
+      expect(find('.filtered-labels')).to have_content(wontfix.title)
+
+      find('.js-label-select').click
+      wait_for_ajax
+      find('.dropdown-menu-labels a', text: label.title).click
+
+      find('body').click
+
+      expect(find('.filtered-labels')).to have_content(wontfix.title)
+      expect(find('.filtered-labels')).to have_content(label.title)
+
+      find('.js-label-select').click
+      wait_for_ajax
+
+      expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
+      expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
+    end
+
+    it "selects and unselects `won't fix`" do
+      find('.dropdown-menu-labels a', text: wontfix.title).click
+      find('.dropdown-menu-labels a', text: wontfix.title).click
+      # Close label dropdown to load
+      find('body').click
+      expect(page).not_to have_css('.filtered-labels')
+    end
   end
 
-  describe 'Filter issues for assignee and label from issues#index' do
+  describe 'for assignee and label from issues#index' do
     before do
       visit namespace_project_issues_path(project.namespace, project)
 
@@ -169,7 +216,7 @@ describe 'Filter issues', feature: true do
 
     context 'only text', js: true do
       it 'filters issues by searched text' do
-        fill_in 'issue_search', with: 'Bug'
+        fill_in 'issuable_search', with: 'Bug'
 
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 2)
@@ -177,7 +224,7 @@ describe 'Filter issues', feature: true do
       end
 
       it 'does not show any issues' do
-        fill_in 'issue_search', with: 'testing'
+        fill_in 'issuable_search', with: 'testing'
 
         page.within '.issues-list' do
           expect(page).not_to have_selector('.issue')
@@ -187,8 +234,9 @@ describe 'Filter issues', feature: true do
 
     context 'text and dropdown options', js: true do
       it 'filters by text and label' do
-        fill_in 'issue_search', with: 'Bug'
+        fill_in 'issuable_search', with: 'Bug'
 
+        expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 2)
         end
@@ -199,14 +247,16 @@ describe 'Filter issues', feature: true do
         end
         find('.dropdown-menu-close-icon').click
 
+        expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 1)
         end
       end
 
       it 'filters by text and milestone' do
-        fill_in 'issue_search', with: 'Bug'
+        fill_in 'issuable_search', with: 'Bug'
 
+        expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 2)
         end
@@ -216,14 +266,16 @@ describe 'Filter issues', feature: true do
           click_link '8'
         end
 
+        expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 1)
         end
       end
 
       it 'filters by text and assignee' do
-        fill_in 'issue_search', with: 'Bug'
+        fill_in 'issuable_search', with: 'Bug'
 
+        expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 2)
         end
@@ -233,14 +285,16 @@ describe 'Filter issues', feature: true do
           click_link user.name
         end
 
+        expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 1)
         end
       end
 
       it 'filters by text and author' do
-        fill_in 'issue_search', with: 'Bug'
+        fill_in 'issuable_search', with: 'Bug'
 
+        expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 2)
         end
@@ -250,6 +304,7 @@ describe 'Filter issues', feature: true do
           click_link user.name
         end
 
+        expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
         page.within '.issues-list' do
           expect(page).to have_selector('.issue', count: 1)
         end
@@ -278,6 +333,7 @@ describe 'Filter issues', feature: true do
       find('.dropdown-menu-close-icon').click
       wait_for_ajax
 
+      expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
       page.within '.issues-list' do
         expect(page).to have_selector('.issue', count: 2)
       end
@@ -293,4 +349,36 @@ describe 'Filter issues', feature: true do
       end
     end
   end
+
+  it 'updates atom feed link for project issues' do
+    visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id)
+
+    link = find('.nav-controls a', text: '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)
+
+    expect(params).to include('private_token' => [user.private_token])
+    expect(params).to include('milestone_title' => [''])
+    expect(params).to include('assignee_id' => [user.id.to_s])
+    expect(auto_discovery_params).to include('private_token' => [user.private_token])
+    expect(auto_discovery_params).to include('milestone_title' => [''])
+    expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+  end
+
+  it 'updates atom feed link for group issues' do
+    visit issues_group_path(group, milestone_title: '', assignee_id: user.id)
+
+    link = find('.nav-controls a', text: '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)
+
+    expect(params).to include('private_token' => [user.private_token])
+    expect(params).to include('milestone_title' => [''])
+    expect(params).to include('assignee_id' => [user.id.to_s])
+    expect(auto_discovery_params).to include('private_token' => [user.private_token])
+    expect(auto_discovery_params).to include('milestone_title' => [''])
+    expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+  end
 end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8771cc8e157739451b05ba8eb366d9a79bfcb254
--- /dev/null
+++ b/spec/features/issues/form_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'New/edit issue', feature: true, js: true do
+  let!(:project)   { create(:project) }
+  let!(:user)      { create(:user)}
+  let!(:milestone) { create(:milestone, project: project) }
+  let!(:label)     { create(:label, project: project) }
+  let!(:label2)    { create(:label, project: project) }
+  let!(:issue)     { create(:issue, project: project, assignee: user, milestone: milestone) }
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+  end
+
+  context 'new issue' do
+    before do
+      visit new_namespace_project_issue_path(project.namespace, project)
+    end
+
+    it 'allows user to create new issue' do
+      fill_in 'issue_title', with: 'title'
+      fill_in 'issue_description', with: 'title'
+
+      click_button 'Assignee'
+      page.within '.dropdown-menu-user' do
+        click_link user.name
+      end
+      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
+
+      click_button 'Milestone'
+      page.within '.issue-milestone' do
+        click_link milestone.title
+      end
+      expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+      page.within '.js-milestone-select' do
+        expect(page).to have_content milestone.title
+      end
+
+      click_button 'Labels'
+      page.within '.dropdown-menu-labels' do
+        click_link label.title
+        click_link label2.title
+      end
+      page.within '.js-label-select' do
+        expect(page).to have_content label.title
+      end
+      expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+      expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+      click_button 'Submit issue'
+
+      page.within '.issuable-sidebar' do
+        page.within '.assignee' do
+          expect(page).to have_content user.name
+        end
+
+        page.within '.milestone' do
+          expect(page).to have_content milestone.title
+        end
+
+        page.within '.labels' do
+          expect(page).to have_content label.title
+          expect(page).to have_content label2.title
+        end
+      end
+    end
+  end
+
+  context 'edit issue' do
+    before do
+      visit edit_namespace_project_issue_path(project.namespace, project, issue)
+    end
+
+    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)
+
+      page.within '.js-user-search' do
+        expect(page).to have_content user.name
+      end
+
+      page.within '.js-milestone-select' do
+        expect(page).to have_content milestone.title
+      end
+
+      click_button 'Labels'
+      page.within '.dropdown-menu-labels' do
+        click_link label.title
+        click_link label2.title
+      end
+      page.within '.js-label-select' do
+        expect(page).to have_content label.title
+      end
+      expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+      expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+      click_button 'Save changes'
+
+      page.within '.issuable-sidebar' do
+        page.within '.assignee' do
+          expect(page).to have_content user.name
+        end
+
+        page.within '.milestone' do
+          expect(page).to have_content milestone.title
+        end
+
+        page.within '.labels' do
+          expect(page).to have_content label.title
+          expect(page).to have_content label2.title
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 7773c486b4e8aeff5cf280c9180205f92f95b259..055210399a74f6bcff282822c20f780a3013e4bc 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -55,7 +55,7 @@ feature 'issue move to another project' do
         first('.select2-choice').click
       end
 
-      fill_in('s2id_autogen2_search', with: new_project_search.name)
+      fill_in('s2id_autogen1_search', with: new_project_search.name)
 
       page.within '.select2-drop' do
         expect(page).to have_content(new_project_search.name)
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index e528aff4d41454920daab187a0c44d1bdf8e98b5..ab901e746172e398c32096c3dd54816e623868da 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -18,22 +18,24 @@ feature 'Start new branch from an issue', feature: true do
     end
 
     context "when there is a referenced merge request" do
-      let(:note) do
-        create(:note, :on_issue, :system, project: project,
-                                          note: "mentioned in !#{referenced_mr.iid}")
+      let!(:note) do
+        create(:note, :on_issue, :system, project: project, noteable: issue,
+                                          note: "Mentioned in !#{referenced_mr.iid}")
       end
+
       let(:referenced_mr) do
         create(:merge_request, :simple, source_project: project, target_project: project,
                                         description: "Fixes ##{issue.iid}", author: user)
       end
 
       before do
-        issue.notes << note
+        referenced_mr.cache_merge_request_closes_issues!(user)
 
         visit namespace_project_issue_path(project.namespace, project, issue)
       end
 
       it "hides the new branch button", js: true do
+        expect(page).to have_css('#new-branch .unavailable')
         expect(page).not_to have_css('#new-branch .available')
         expect(page).to have_content /1 Related Merge Request/
       end
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c9a3ecf16ea1ae528bbb456eba4a80647f6d339e
--- /dev/null
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+feature 'Issues filter reset button', feature: true, js: true do
+  include WaitForAjax
+  include IssueHelpers
+
+  let!(:project)    { create(:project, :public) }
+  let!(:user)        { create(:user)}
+  let!(:milestone)  { create(:milestone, project: project) }
+  let!(:bug)        { create(:label, project: project, name: 'bug')}
+  let!(:issue1)     { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
+  let!(:issue2)     { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
+
+  before do
+    project.team << [user, :developer]
+  end
+
+  context 'when a milestone filter has been applied' do
+    it 'resets the milestone filter' do
+      visit_issues(project, milestone_title: milestone.title)
+      expect(page).to have_css('.issue', count: 1)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when a label filter has been applied' do
+    it 'resets the label filter' do
+      visit_issues(project, label_name: bug.name)
+      expect(page).to have_css('.issue', count: 1)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when a text search has been conducted' do
+    it 'resets the text search filter' do
+      visit_issues(project, search: 'Bug')
+      expect(page).to have_css('.issue', count: 1)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when author filter has been applied' do
+    it 'resets the author filter' do
+      visit_issues(project, author_id: user.id)
+      expect(page).to have_css('.issue', count: 1)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when assignee filter has been applied' do
+    it 'resets the assignee filter' do
+      visit_issues(project, assignee_id: user.id)
+      expect(page).to have_css('.issue', count: 1)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when all filters have been applied' do
+    it 'resets all filters' do
+      visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+      expect(page).to have_css('.issue', count: 0)
+
+      reset_filters
+      expect(page).to have_css('.issue', count: 2)
+    end
+  end
+
+  context 'when no filters have been applied' do
+    it 'the reset link should not be visible' do
+      visit_issues(project)
+      expect(page).to have_css('.issue', count: 2)
+      expect(page).not_to have_css '.reset_filters'
+    end
+  end
+
+  def reset_filters
+    find('.reset-filters').click
+  end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3f2da1c380c0e0c13d11f03a21c54effe3df58cf
--- /dev/null
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -0,0 +1,113 @@
+require 'rails_helper'
+
+feature 'Issues > User uses slash commands', feature: true, js: true do
+  include SlashCommandsHelpers
+  include WaitForAjax
+
+  it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
+    let(:issuable) { create(:issue, project: project) }
+  end
+
+  describe 'issue-only commands' do
+    let(:user) { create(:user) }
+    let(:project) { create(:project, :public) }
+
+    before do
+      project.team << [user, :master]
+      login_with(user)
+      visit namespace_project_issue_path(project.namespace, project, issue)
+    end
+
+    after do
+      wait_for_ajax
+    end
+
+    describe 'adding a due date from note' do
+      let(:issue) { create(:issue, project: project) }
+
+      context 'when the current user can update the due date' do
+        it 'does not create a note, and sets the due date accordingly' do
+          write_note("/due 2016-08-28")
+
+          expect(page).not_to have_content '/due 2016-08-28'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          issue.reload
+
+          expect(issue.due_date).to eq Date.new(2016, 8, 28)
+        end
+      end
+
+      context 'when the current user cannot update the due date' do
+        let(:guest) { create(:user) }
+        before do
+          project.team << [guest, :guest]
+          logout
+          login_with(guest)
+          visit namespace_project_issue_path(project.namespace, project, issue)
+        end
+
+        it 'does not create a note, and sets the due date accordingly' do
+          write_note("/due 2016-08-28")
+
+          expect(page).to have_content '/due 2016-08-28'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          issue.reload
+
+          expect(issue.due_date).to be_nil
+        end
+      end
+    end
+
+    describe 'removing a due date from note' do
+      let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
+
+      context 'when the current user can update the due date' do
+        it 'does not create a note, and removes the due date accordingly' do
+          expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+          write_note("/remove_due_date")
+
+          expect(page).not_to have_content '/remove_due_date'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          issue.reload
+
+          expect(issue.due_date).to be_nil
+        end
+      end
+
+      context 'when the current user cannot update the due date' do
+        let(:guest) { create(:user) }
+        before do
+          project.team << [guest, :guest]
+          logout
+          login_with(guest)
+          visit namespace_project_issue_path(project.namespace, project, issue)
+        end
+
+        it 'does not create a note, and sets the due date accordingly' do
+          write_note("/remove_due_date")
+
+          expect(page).to have_content '/remove_due_date'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          issue.reload
+
+          expect(issue.due_date).to eq Date.new(2016, 8, 28)
+        end
+      end
+    end
+
+    describe 'toggling the WIP prefix from the title from note' do
+      let(:issue) { create(:issue, project: project) }
+
+      it 'does not recognize the command nor create a note' do
+        write_note("/wip")
+
+        expect(page).not_to have_content '/wip'
+      end
+    end
+  end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index cb445e22af0a5aeb20bb200bba8205ae826705dd..cdd02a8c8e36a012ebd7b775646dce009810b997 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
 describe 'Issues', feature: true do
   include IssueHelpers
   include SortingHelper
+  include WaitForAjax
 
   let(:project) { create(:project) }
 
@@ -51,9 +52,8 @@ describe 'Issues', feature: true do
 
       expect(page).to have_content "Assignee #{@user.name}"
 
-      first('#s2id_issue_assignee_id').click
-      sleep 2 # wait for ajax stuff to complete
-      first('.user-result').click
+      first('.js-user-search').click
+      click_link 'Unassigned'
 
       click_button 'Save changes'
 
@@ -122,6 +122,17 @@ describe 'Issues', feature: true do
           expect(page).to have_content date.to_s(:medium)
         end
       end
+
+      it 'warns about version conflict' do
+        issue.update(title: "New title")
+
+        fill_in 'issue_title', with: 'bug 345'
+        fill_in 'issue_description', with: 'bug description'
+
+        click_button 'Save changes'
+
+        expect(page).to have_content 'Someone edited the issue the same time you did'
+      end
     end
   end
 
@@ -133,7 +144,7 @@ describe 'Issues', feature: true do
       visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
 
       expect(page).to have_content 'foobar'
-      expect(page.all('.issue-no-comments').first.text).to eq "0"
+      expect(page.all('.no-comments').first.text).to eq "0"
     end
   end
 
@@ -358,6 +369,44 @@ describe 'Issues', feature: true do
     end
   end
 
+  describe 'when I want to reset my incoming email token' do
+    let(:project1) { create(:project, namespace: @user.namespace) }
+
+    before do
+      allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+      project1.team << [@user, :master]
+      visit namespace_project_issues_path(@user.namespace, project1)
+    end
+
+    it 'changes incoming email address token', js: true do
+      find('.issue-email-modal-btn').click
+      previous_token = find('input#issue_email').value
+
+      find('.incoming-email-token-reset').click
+      wait_for_ajax
+
+      expect(find('input#issue_email').value).not_to eq(previous_token)
+    end
+  end
+
+  describe 'update labels from issue#show', js: true do
+    let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+    let!(:label) { create(:label, project: project) }
+
+    before do
+      visit namespace_project_issue_path(project.namespace, project, issue)
+    end
+
+    it 'will not send ajax request when no data is changed' do
+      page.within '.labels' do
+        click_link 'Edit'
+        first('.dropdown-menu-close').click
+
+        expect(page).not_to have_selector('.block-loading')
+      end
+    end
+  end
+
   describe 'update assignee from issue#show' do
     let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
 
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 2523b4b78982c6d5afe78a1ca9d66a3ed8aae604..76bcfbe523a4a113a047a9908573288706703015 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -29,7 +29,7 @@ feature 'Login', feature: true do
 
   describe 'with two-factor authentication' do
     def enter_code(code)
-      fill_in 'Two-Factor Authentication code', with: code
+      fill_in 'user_otp_attempt', with: code
       click_button 'Verify code'
     end
 
@@ -215,4 +215,69 @@ feature 'Login', feature: true do
       end
     end
   end
+
+  describe 'UI tabs and panes' do
+    context 'when no defaults are changed' do
+      it 'correctly renders tabs and panes' do
+        ensure_tab_pane_correctness
+      end
+    end
+
+    context 'when signup is disabled' do
+      before do
+        stub_application_setting(signup_enabled: false)
+      end
+
+      it 'correctly renders tabs and panes' do
+        ensure_tab_pane_correctness
+      end
+    end
+
+    context 'when ldap is enabled' do
+      before do
+        visit new_user_session_path
+        allow(page).to receive(:form_based_providers).and_return([:ldapmain])
+        allow(page).to receive(:ldap_enabled).and_return(true)
+      end
+
+      it 'correctly renders tabs and panes' do
+        ensure_tab_pane_correctness(false)
+      end
+    end
+
+    context 'when crowd is enabled' do
+      before do
+        visit new_user_session_path
+        allow(page).to receive(:form_based_providers).and_return([:crowd])
+        allow(page).to receive(:crowd_enabled?).and_return(true)
+      end
+
+      it 'correctly renders tabs and panes' do
+        ensure_tab_pane_correctness(false)
+      end
+    end
+
+    def ensure_tab_pane_correctness(visit_path = true)
+      if visit_path
+        visit new_user_session_path
+      end
+
+      ensure_tab_pane_counts
+      ensure_one_active_tab
+      ensure_one_active_pane
+    end
+
+    def ensure_tab_pane_counts
+      tabs_count = page.all('[role="tab"]').size
+      expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
+    end
+
+    def ensure_one_active_tab
+      expect(page).to have_selector('.nav-tabs > li.active', count: 1)
+    end
+
+    def ensure_one_active_pane
+      expect(page).to have_selector('.tab-pane.active', count: 1)
+    end
+  end
 end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..43cc6f2a2a7d7ea3bebde280b94b629784e47189
--- /dev/null
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -0,0 +1,51 @@
+require 'rails_helper'
+
+feature 'Merge request issue assignment', js: true, feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:issue1) { create(:issue, project: project) }
+  let(:issue2) { create(:issue, project: project) }
+  let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue1.to_reference} and #{issue2.to_reference}") }
+  let(:service) { MergeRequests::AssignIssuesService.new(merge_request, user, user, project) }
+
+  before do
+    project.team << [user, :developer]
+  end
+
+  def visit_merge_request(current_user = nil)
+    login_as(current_user || user)
+    visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+  end
+
+  context 'logged in as author' do
+    scenario 'updates related issues' do
+      visit_merge_request
+      click_link "Assign yourself to these issues"
+
+      expect(page).to have_content "2 issues have been assigned to you"
+    end
+
+    it 'returns user to the merge request' do
+      visit_merge_request
+      click_link "Assign yourself to these issues"
+
+      expect(page).to have_content merge_request.description
+    end
+
+    it "doesn't display if related issues are already assigned" do
+      [issue1, issue2].each { |issue| issue.update!(assignee: user) }
+
+      visit_merge_request
+
+      expect(page).not_to have_content "Assign yourself"
+    end
+  end
+
+  context 'not MR author' do
+    it "doesn't not show assignment link" do
+      visit_merge_request(create(:user))
+
+      expect(page).not_to have_content "Assign yourself"
+    end
+  end
+end
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f11db3c4170d8a0520cddbd1443b1ece9d7e153
--- /dev/null
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+feature 'Check if mergeable with unresolved discussions', js: true, feature: true do
+  let(:user)           { create(:user) }
+  let(:project)        { create(:project) }
+  let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+
+  before do
+    login_as user
+    project.team << [user, :master]
+  end
+
+  context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
+    before do
+      project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+    end
+
+    context 'with unresolved discussions' do
+      it 'does not allow to merge' do
+        visit_merge_request(merge_request)
+
+        expect(page).not_to have_button 'Accept Merge Request'
+        expect(page).to have_content('This merge request has unresolved discussions')
+      end
+    end
+
+    context 'with all discussions resolved' do
+      before do
+        merge_request.discussions.each { |d| d.resolve!(user) }
+      end
+
+      it 'allows MR to be merged' do
+        visit_merge_request(merge_request)
+
+        expect(page).to have_button 'Accept Merge Request'
+      end
+    end
+  end
+
+  context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
+    before do
+      project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+    end
+
+    context 'with unresolved discussions' do
+      it 'does not allow to merge' do
+        visit_merge_request(merge_request)
+
+        expect(page).to have_button 'Accept Merge Request'
+      end
+    end
+
+    context 'with all discussions resolved' do
+      before do
+        merge_request.discussions.each { |d| d.resolve!(user) }
+      end
+
+      it 'allows MR to be merged' do
+        visit_merge_request(merge_request)
+
+        expect(page).to have_button 'Accept Merge Request'
+      end
+    end
+  end
+
+  def visit_merge_request(merge_request)
+    visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+  end
+end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d258ff52bbb7eb326cc77cdb569302f96a4ff5aa
--- /dev/null
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+feature 'Merge request conflict resolution', js: true, feature: true do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  def create_merge_request(source_branch)
+    create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+      mr.mark_as_unmergeable
+    end
+  end
+
+  shared_examples "conflicts are resolved in Interactive mode" do
+    it 'conflicts are resolved in Interactive mode' do
+      within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+        click_button 'Use ours'
+      end
+
+      within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+        all('button', text: 'Use ours').each do |button|
+          button.click
+        end
+      end
+
+      click_button 'Commit conflict resolution'
+      wait_for_ajax
+
+      expect(page).to have_content('All merge conflicts were resolved')
+      merge_request.reload_diff
+
+      click_on 'Changes'
+      wait_for_ajax
+
+      within find('.diff-file', text: 'files/ruby/popen.rb') do
+        expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }")
+        expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }")
+      end
+
+      within find('.diff-file', text: 'files/ruby/regex.rb') do
+        expect(page).to have_selector('.line_content.new', text: "def username_regexp")
+        expect(page).to have_selector('.line_content.new', text: "def project_name_regexp")
+        expect(page).to have_selector('.line_content.new', text: "def path_regexp")
+        expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp")
+        expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp")
+        expect(page).to have_selector('.line_content.new', text: "def default_regexp")
+      end
+    end
+  end
+
+  shared_examples "conflicts are resolved in Edit inline mode" do
+    it 'conflicts are resolved in Edit inline mode' do
+      expect(find('#conflicts')).to have_content('popen.rb')
+
+      within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
+        click_button 'Edit inline'
+        wait_for_ajax
+        execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
+      end
+
+      within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
+        click_button 'Edit inline'
+        wait_for_ajax
+        execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
+      end
+
+      click_button 'Commit conflict resolution'
+      wait_for_ajax
+      expect(page).to have_content('All merge conflicts were resolved')
+      merge_request.reload_diff
+
+      click_on 'Changes'
+      wait_for_ajax
+
+      expect(page).to have_content('One morning')
+      expect(page).to have_content('Gregor Samsa woke from troubled dreams')
+    end
+  end
+
+  context 'can be resolved in the UI' do
+    before do
+      project.team << [user, :developer]
+      login_as(user)
+    end
+
+    context 'the conflicts are resolvable' do
+      let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+      before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) }
+
+      it 'shows a link to the conflict resolution page' do
+        expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+      end
+
+      context 'in Inline view mode' do
+        before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+        include_examples "conflicts are resolved in Interactive mode"
+        include_examples "conflicts are resolved in Edit inline mode"
+      end
+
+      context 'in Parallel view mode' do
+        before do
+          click_link('conflicts', href: /\/conflicts\Z/) 
+          click_button 'Side-by-side'
+        end
+
+        include_examples "conflicts are resolved in Interactive mode"
+        include_examples "conflicts are resolved in Edit inline mode"
+      end
+    end
+
+    context 'the conflict contain markers' do
+      let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
+
+      before do
+        visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+        click_link('conflicts', href: /\/conflicts\Z/)
+      end
+
+      it 'conflicts can not be resolved in Interactive mode' do
+        within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+          expect(page).not_to have_content 'Interactive mode'
+          expect(page).not_to have_content 'Edit inline'
+        end
+      end
+
+      it 'conflicts are resolved in Edit inline mode' do
+        within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
+          wait_for_ajax
+          execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
+        end
+
+        click_button 'Commit conflict resolution'
+        wait_for_ajax
+
+        expect(page).to have_content('All merge conflicts were resolved')
+
+        merge_request.reload_diff
+
+        click_on 'Changes'
+        wait_for_ajax
+        find('.click-to-expand').click
+        wait_for_ajax
+
+        expect(page).to have_content('Gregor Samsa woke from troubled dreams')
+      end
+    end
+  end
+
+  UNRESOLVABLE_CONFLICTS = {
+    'conflict-too-large' => 'when the conflicts contain a large file',
+    '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',
+  }
+
+  UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
+    context description do
+      let(:merge_request) { create_merge_request(source_branch) }
+
+      before do
+        project.team << [user, :developer]
+        login_as(user)
+
+        visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+      end
+
+      it 'does not show a link to the conflict resolution page' do
+        expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+      end
+
+      it 'shows an error if the conflicts page is visited directly' do
+        visit current_url + '/conflicts'
+        wait_for_ajax
+
+        expect(find('#conflicts')).to have_content('Please try to resolve them locally.')
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 11c9de3c4bf29617f2ed326c578ada3aca7df097..c68e1ea4af964a4ef725649c4ba8b1df4a49a76f 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -8,10 +8,11 @@ feature 'Create New Merge Request', feature: true, js: true do
     project.team << [user, :master]
 
     login_as user
-    visit namespace_project_merge_requests_path(project.namespace, project)
   end
 
   it 'generates a diff for an orphaned branch' do
+    visit namespace_project_merge_requests_path(project.namespace, project)
+
     click_link 'New Merge Request'
     expect(page).to have_content('Source branch')
     expect(page).to have_content('Target branch')
@@ -42,4 +43,28 @@ feature 'Create New Merge Request', feature: true, js: true do
       expect(page).not_to have_content private_project.to_reference
     end
   end
+
+  it 'allows to change the diff view' do
+    visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+    click_link 'Changes'
+
+    expect(page).to have_css('a.btn.active', text: 'Inline')
+    expect(page).not_to have_css('a.btn.active', text: 'Side-by-side')
+
+    click_link 'Side-by-side'
+
+    within '.merge-request' do
+      expect(page).not_to have_css('a.btn.active', text: 'Inline')
+      expect(page).to have_css('a.btn.active', text: 'Side-by-side')
+    end
+  end
+
+  it 'does not allow non-existing branches' do
+    visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
+
+    expect(page).to have_content('The form contains the following errors')
+    expect(page).to have_content('Source branch "non-exist-source" does not exist')
+    expect(page).to have_content('Target branch "non-exist-target" does not exist')
+  end
 end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 4d5d4aa121add23e76759dc7de4fd0f318ac3001..142649297cc8342f4eb84009f693ba5e1e9f7206 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -25,9 +25,21 @@ feature 'Merge request created from fork' do
     expect(page).to have_content 'Test merge request'
   end
 
-  context 'pipeline present in source project' do
-    include WaitForAjax
+  context 'source project is deleted' do
+    background do
+      MergeRequests::MergeService.new(project, user).execute(merge_request)
+      fork_project.destroy!
+    end
+
+    scenario 'user can access merge request' do
+      visit_merge_request(merge_request)
 
+      expect(page).to have_content 'Test merge request'
+      expect(page).to have_content "(removed):#{merge_request.source_branch}"
+    end
+  end
+
+  context 'pipeline present in source project' do
     given(:pipeline) do
       create(:ci_pipeline,
              project: fork_project,
@@ -43,9 +55,8 @@ feature 'Merge request created from fork' do
     scenario 'user visits a pipelines page', js: true do
       visit_merge_request(merge_request)
       page.within('.merge-request-tabs') { click_link 'Builds' }
-      wait_for_ajax
 
-      page.within('table.builds') do
+      page.within('table.ci-table') do
         expect(page).to have_content 'rspec'
         expect(page).to have_content 'spinach'
       end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e6d84672172968b80223cf4a5251bf2181b1a9d
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -0,0 +1,499 @@
+require 'spec_helper'
+
+feature 'Diff notes resolve', feature: true, js: true do
+  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!(:note)         { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+  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
+
+  context 'no discussions' do
+    before do
+      project.team << [user, :master]
+      login_as user
+      note.destroy
+      visit_merge_request
+    end
+
+    it 'displays no discussion resolved data' do
+      expect(page).not_to have_content('discussion resolved')
+      expect(page).not_to have_selector('.discussion-next-btn')
+    end
+  end
+
+  context 'as authorized user' do
+    before do
+      project.team << [user, :master]
+      login_as user
+      visit_merge_request
+    end
+
+    context 'single discussion' do
+      it 'shows text with how many discussions' do
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'allows user to mark a note as resolved' do
+        page.within '.diff-content .note' do
+          find('.line-resolve-btn').click
+
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+          expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+        end
+
+        page.within '.diff-content' do
+          expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to mark discussion as resolved' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+        end
+
+        page.within '.diff-content .note' do
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+
+          expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to unresolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+          click_button 'Unresolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'hides resolved discussion' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+        end
+
+        visit_merge_request
+
+        expect(page).to have_selector('.discussion-body', visible: false)
+      end
+
+      it 'allows user to resolve from reply form without a comment' do
+        page.within '.diff-content' do
+          click_button 'Reply...'
+
+          click_button 'Resolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to unresolve from reply form without a comment' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+          sleep 1
+
+          click_button 'Reply...'
+
+          click_button 'Unresolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+          expect(page).not_to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to comment & resolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Reply...'
+
+          find('.js-note-text').set 'testing'
+
+          click_button 'Comment & resolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to comment & unresolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+
+          click_button 'Reply...'
+
+          find('.js-note-text').set 'testing'
+
+          click_button 'Comment & unresolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'allows user to quickly scroll to next unresolved discussion' do
+        page.within '.line-resolve-all-container' do
+          page.find('.discussion-next-btn').click
+        end
+
+        expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+      end
+
+      it 'hides jump to next button when all resolved' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+        end
+
+        expect(page).to have_selector('.discussion-next-btn', visible: false)
+      end
+
+      it 'updates updated text after resolving note' do
+        page.within '.diff-content .note' do
+          find('.line-resolve-btn').click
+        end
+
+        expect(page).to have_content("Resolved by #{user.name}")
+      end
+
+      it 'hides jump to next discussion button' do
+        page.within '.discussion-reply-holder' do
+          expect(page).not_to have_selector('.discussion-next-btn')
+        end
+      end
+    end
+
+    context 'multiple notes' do
+      before do
+        create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+        visit_merge_request
+      end
+
+      it 'does not mark discussion as resolved when resolving single note' do
+        page.first '.diff-content .note' do
+          first('.line-resolve-btn').click
+          expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+        end
+
+        expect(page).to have_content('Last updated')
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'resolves discussion' do
+        page.all('.note').each do |note|
+          note.all('.line-resolve-btn').each do |button|
+            button.click
+          end
+        end
+
+        expect(page).to have_content('Resolved by')
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+        end
+      end
+    end
+
+    context 'muliple discussions' do
+      before do
+        create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
+        visit_merge_request
+      end
+
+      it 'shows text with how many discussions' do
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/2 discussions resolved')
+        end
+      end
+
+      it 'allows user to mark a single note as resolved' do
+        click_button('Resolve discussion', match: :first)
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/2 discussions resolved')
+        end
+      end
+
+      it 'allows user to mark all notes as resolved' do
+        page.all('.line-resolve-btn').each do |btn|
+          btn.click
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('2/2 discussions resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user user to mark all discussions as resolved' do
+        page.all('.discussion-reply-holder').each do |reply_holder|
+          page.within reply_holder do
+            click_button 'Resolve discussion'
+          end
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('2/2 discussions resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to quickly scroll to next unresolved discussion' do
+        page.within first('.discussion-reply-holder') do
+          click_button 'Resolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          page.find('.discussion-next-btn').click
+        end
+
+        expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+      end
+
+      it 'updates updated text after resolving note' do
+        page.within first('.diff-content .note') do
+          find('.line-resolve-btn').click
+        end
+
+        expect(page).to have_content("Resolved by #{user.name}")
+      end
+
+      it 'shows jump to next discussion button' do
+        page.all('.discussion-reply-holder').each do |holder|
+          expect(holder).to have_selector('.discussion-next-btn')
+        end
+      end
+
+      it 'displays next discussion even if hidden' do
+        page.all('.note-discussion').each do |discussion|
+          page.within discussion do
+            click_link 'Toggle discussion'
+          end
+        end
+
+        page.within('.issuable-discussion #notes') do
+          expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+        end
+
+        page.within '.line-resolve-all-container' do
+          page.find('.discussion-next-btn').click
+        end
+
+        expect(find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+      end
+    end
+
+    context 'changes tab' do
+      it 'shows text with how many discussions' do
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'allows user to mark a note as resolved' do
+        page.within '.diff-content .note' do
+          find('.line-resolve-btn').click
+
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+
+        page.within '.diff-content' do
+          expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to mark discussion as resolved' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+        end
+
+        page.within '.diff-content .note' do
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to unresolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+          click_button 'Unresolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'allows user to comment & resolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Reply...'
+
+          find('.js-note-text').set 'testing'
+
+          click_button 'Comment & resolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+
+      it 'allows user to comment & unresolve discussion' do
+        page.within '.diff-content' do
+          click_button 'Resolve discussion'
+
+          click_button 'Reply...'
+
+          find('.js-note-text').set 'testing'
+
+          click_button 'Comment & unresolve discussion'
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+    end
+  end
+
+  context 'as a guest' do
+    let(:guest) { create(:user) }
+
+    before do
+      project.team << [guest, :guest]
+      login_as guest
+    end
+
+    context 'someone elses merge request' do
+      before do
+        visit_merge_request
+      end
+
+      it 'does not allow user to mark note as resolved' do
+        page.within '.diff-content .note' do
+          expect(page).not_to have_selector('.line-resolve-btn')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+
+      it 'does not allow user to mark discussion as resolved' do
+        page.within '.diff-content .note' do
+          expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+        end
+      end
+    end
+
+    context 'guest users merge request' do
+      before do
+        mr = create(:merge_request_with_diffs, source_project: project, source_branch: 'markdown', author: guest, title: "Bug")
+        create(:diff_note_on_merge_request, project: project, noteable: mr)
+        visit_merge_request(mr)
+      end
+
+      it 'allows user to mark a note as resolved' do
+        page.within '.diff-content .note' do
+          find('.line-resolve-btn').click
+
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+
+        page.within '.diff-content' do
+          expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('1/1 discussion resolved')
+          expect(page).to have_selector('.line-resolve-btn.is-active')
+        end
+      end
+    end
+  end
+
+  context 'unauthorized user' do
+    context 'no resolved comments' do
+      before do
+        visit_merge_request
+      end
+
+      it 'does not allow user to mark note as resolved' do
+        page.within '.diff-content .note' do
+          expect(page).not_to have_selector('.line-resolve-btn')
+        end
+
+        page.within '.line-resolve-all-container' do
+          expect(page).to have_content('0/1 discussion resolved')
+        end
+      end
+    end
+
+    context 'resolved comment' do
+      before do
+        note.resolve!(user)
+        visit_merge_request
+      end
+
+      it 'shows resolved icon' do
+        expect(page).to have_content '1/1 discussion resolved'
+
+        click_link 'Toggle discussion'
+        expect(page).to have_selector('.line-resolve-btn.is-active')
+      end
+
+      it 'does not allow user to click resolve button' do
+        expect(page).to have_selector('.line-resolve-btn.is-disabled')
+        click_link 'Toggle discussion'
+
+        expect(page).to have_selector('.line-resolve-btn.is-disabled')
+      end
+    end
+  end
+
+  def visit_merge_request(mr = nil)
+    mr = mr || merge_request
+    visit namespace_project_merge_request_path(mr.project.namespace, mr.project, mr)
+  end
+end
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06fad1007e8135d926c8ea9ef84445eec98dc0ce
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -0,0 +1,238 @@
+require 'spec_helper'
+
+feature 'Diff notes', js: true, feature: true do
+  include WaitForAjax
+
+  before do
+    login_as :admin
+    @merge_request = create(:merge_request)
+    @project = @merge_request.source_project
+  end
+
+  context 'merge request diffs' do
+    let(:comment_button_class) { '.add-diff-note' }
+    let(:notes_holder_input_class) { 'js-temp-notes-holder' }
+    let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
+    let(:test_note_comment) { 'this is a test note!' }
+
+    context 'when hovering over a parallel view diff file' do
+      before(:each) do
+        visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
+      end
+
+      context 'with an old line on the left and no line on the right' do
+        it 'should allow commenting on the left side' do
+          should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
+        end
+
+        it 'should not allow commenting on the right side' do
+          should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+        end
+      end
+
+      context 'with no line on the left and a new line on the right' do
+        it 'should not allow commenting on the left side' do
+          should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+        end
+
+        it 'should allow commenting on the right side' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+        end
+      end
+
+      context 'with an old line on the left and a new line on the right' do
+        it 'should allow commenting on the left side' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+        end
+
+        it 'should allow commenting on the right side' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+        end
+      end
+
+      context 'with an unchanged line on the left and an unchanged line on the right' do
+        it 'should allow commenting on the left side' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+        end
+
+        it 'should allow commenting on the right side' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+        end
+      end
+
+      context 'with a match line' do
+        it 'should not allow commenting on the left side' do
+          should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+        end
+
+        it 'should not allow commenting on the right side' do
+          should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+        end
+      end
+
+      context 'with an unfolded line' do
+        before(:each) do
+          find('.js-unfold', match: :first).click
+          wait_for_ajax
+        end
+
+        # The first `.js-unfold` unfolds upwards, therefore the first
+        # `.line_holder` will be an unfolded line.
+        let(:line_holder) { first('.line_holder[id="1"]') }
+
+        it 'should not allow commenting on the left side' do
+          should_not_allow_commenting(line_holder, 'left')
+        end
+
+        it 'should not allow commenting on the right side' do
+          should_not_allow_commenting(line_holder, 'right')
+        end
+      end
+    end
+
+    context 'when hovering over an inline view diff file' do
+      before do
+        visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+      end
+
+      context 'with a new line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+        end
+      end
+
+      context 'with an old line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+        end
+      end
+
+      context 'with an unchanged line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+        end
+      end
+
+      context 'with a match line' do
+        it 'should not allow commenting' do
+          should_not_allow_commenting(find('.match', match: :first))
+        end
+      end
+
+      context 'with an unfolded line' do
+        before(:each) do
+          find('.js-unfold', match: :first).click
+          wait_for_ajax
+        end
+
+        # The first `.js-unfold` unfolds upwards, therefore the first
+        # `.line_holder` will be an unfolded line.
+        let(:line_holder) { first('.line_holder[id="1"]') }
+
+        it 'should not allow commenting' do
+          should_not_allow_commenting line_holder
+        end
+      end
+
+      context 'when hovering over a diff discussion' do
+        before do
+          visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+          visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+        end
+
+        it 'should not allow commenting' do
+          should_not_allow_commenting(find('.line_holder', match: :first))
+        end
+      end
+    end
+
+    context 'when the MR only supports legacy diff notes' do
+      before do
+        @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+        visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+      end
+
+      context 'with a new line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+        end
+      end
+
+      context 'with an old line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+        end
+      end
+
+      context 'with an unchanged line' do
+        it 'should allow commenting' do
+          should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+        end
+      end
+
+      context 'with a match line' do
+        it 'should not allow commenting' do
+          should_not_allow_commenting(find('.match', match: :first))
+        end
+      end
+    end
+
+    def should_allow_commenting(line_holder, diff_side = nil)
+      line = get_line_components(line_holder, diff_side)
+      line[:content].hover
+      expect(line[:num]).to have_css comment_button_class
+
+      comment_on_line(line_holder, line)
+
+      assert_comment_persistence(line_holder)
+    end
+
+    def should_not_allow_commenting(line_holder, diff_side = nil)
+      line = get_line_components(line_holder, diff_side)
+      line[:content].hover
+      expect(line[:num]).not_to have_css comment_button_class
+    end
+
+    def get_line_components(line_holder, diff_side = nil)
+      if diff_side.nil?
+        get_inline_line_components(line_holder)
+      else
+        get_parallel_line_components(line_holder, diff_side)
+      end
+    end
+
+    def get_inline_line_components(line_holder)
+      { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+    end
+
+    def get_parallel_line_components(line_holder, diff_side = nil)
+      side_index = diff_side == 'left' ? 0 : 1
+      # Wait for `.line_content`
+      line_holder.find('.line_content', match: :first)
+      # Wait for `.diff-line-num`
+      line_holder.find('.diff-line-num', match: :first)
+      { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+    end
+
+    def comment_on_line(line_holder, line)
+      line[:num].find(comment_button_class).trigger 'click'
+      line_holder.find(:xpath, notes_holder_input_xpath)
+
+      notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
+      expect(notes_holder_input[:class]).to include(notes_holder_input_class)
+
+      notes_holder_input.fill_in 'note[note]', with: test_note_comment
+      click_button 'Comment'
+      wait_for_ajax
+    end
+
+    def assert_comment_persistence(line_holder)
+      expect(line_holder).to have_xpath notes_holder_input_xpath
+
+      notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
+      expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
+      expect(notes_holder_saved).to have_content test_note_comment
+    end
+  end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 4109e78f56031f7dcae40811274e58c9111b5ac1..c46bd8d449f376589c9be1acd117ac163a3372bc 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 feature 'Edit Merge Request', feature: true do
   let(:user) { create(:user) }
   let(:project) { create(:project, :public) }
-  let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+  let(:merge_request) { create(:merge_request, :simple, source_project: project) }
 
   before do
     project.team << [user, :master]
@@ -17,5 +17,28 @@ feature 'Edit Merge Request', feature: true do
     it 'has class js-quick-submit in form' do
       expect(page).to have_selector('.js-quick-submit')
     end
+
+    it 'warns about version conflict' do
+      merge_request.update(title: "New title")
+
+      fill_in 'merge_request_title', with: 'bug 345'
+      fill_in 'merge_request_description', with: 'bug description'
+
+      click_button 'Save changes'
+
+      expect(page).to have_content 'Someone edited the merge request the same time you did'
+    end
+
+    it 'allows to unselect "Remove source branch"' do
+      merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
+      expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
+
+      visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+      uncheck 'Remove source branch when merge request is accepted'
+
+      click_button 'Save changes'
+
+      expect(page).to have_content 'Remove source branch'
+    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 bb0bb590a465c2692a1192119f7e297863470a32..f6e9230c8dad26b93514e8bccfa5ee0e44d09f56 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
     visit_merge_requests(project)
     filter_by_milestone(Milestone::None.title)
 
+    expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
     expect(page).to have_css('.merge-request', count: 1)
   end
 
@@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
       visit_merge_requests(project)
       filter_by_milestone(Milestone::Upcoming.title)
 
+      expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
       expect(page).to have_css('.merge-request', count: 1)
     end
 
@@ -61,9 +63,27 @@ feature 'Merge Request filtering by Milestone', feature: true do
     visit_merge_requests(project)
     filter_by_milestone(milestone.title)
 
+    expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
     expect(page).to have_css('.merge-request', count: 1)
   end
 
+  context 'when milestone has single quotes in title' do
+    background do
+      milestone.update(name: "rock 'n' roll")
+    end
+
+    scenario 'filters by a specific Milestone', js: true do
+      create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
+      create(:merge_request, :simple, source_project: project)
+
+      visit_merge_requests(project)
+      filter_by_milestone(milestone.title)
+
+      expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+      expect(page).to have_css('.merge-request', count: 1)
+    end
+  end
+
   def visit_merge_requests(project)
     visit namespace_project_merge_requests_path(project.namespace, project)
   end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7594cbf54e857882625db0903bc1b3649d990507
--- /dev/null
+++ b/spec/features/merge_requests/form_spec.rb
@@ -0,0 +1,273 @@
+require 'rails_helper'
+
+describe 'New/edit merge request', feature: true, js: true do
+  let!(:project)   { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+  let(:fork_project) { create(:project, forked_from_project: project) }
+  let!(:user)      { 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]
+  end
+
+  context 'owned projects' do
+    before do
+      login_as(user)
+    end
+
+    context 'new merge request' do
+      before do
+        visit new_namespace_project_merge_request_path(
+          project.namespace,
+          project,
+          merge_request: {
+            source_project_id: project.id,
+            target_project_id: project.id,
+            source_branch: 'fix',
+            target_branch: 'master'
+          })
+      end
+
+      it 'creates new merge request' do
+        click_button 'Assignee'
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+        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
+        end
+
+        click_button 'Milestone'
+        page.within '.issue-milestone' do
+          click_link milestone.title
+        end
+        expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+        page.within '.js-milestone-select' do
+          expect(page).to have_content milestone.title
+        end
+
+        click_button 'Labels'
+        page.within '.dropdown-menu-labels' do
+          click_link label.title
+          click_link label2.title
+        end
+        page.within '.js-label-select' do
+          expect(page).to have_content label.title
+        end
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+        click_button 'Submit merge request'
+
+        page.within '.issuable-sidebar' do
+          page.within '.assignee' do
+            expect(page).to have_content user.name
+          end
+
+          page.within '.milestone' do
+            expect(page).to have_content milestone.title
+          end
+
+          page.within '.labels' do
+            expect(page).to have_content label.title
+            expect(page).to have_content label2.title
+          end
+        end
+      end
+    end
+
+    context 'edit merge request' do
+      before do
+        merge_request = create(:merge_request,
+                                 source_project: project,
+                                 target_project: project,
+                                 source_branch: 'fix',
+                                 target_branch: 'master'
+                              )
+
+        visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+      end
+
+      it 'updates merge request' do
+        click_button 'Assignee'
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+        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
+        end
+
+        click_button 'Milestone'
+        page.within '.issue-milestone' do
+          click_link milestone.title
+        end
+        expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+        page.within '.js-milestone-select' do
+          expect(page).to have_content milestone.title
+        end
+
+        click_button 'Labels'
+        page.within '.dropdown-menu-labels' do
+          click_link label.title
+          click_link label2.title
+        end
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+        page.within '.js-label-select' do
+          expect(page).to have_content label.title
+        end
+
+        click_button 'Save changes'
+
+        page.within '.issuable-sidebar' do
+          page.within '.assignee' do
+            expect(page).to have_content user.name
+          end
+
+          page.within '.milestone' do
+            expect(page).to have_content milestone.title
+          end
+
+          page.within '.labels' do
+            expect(page).to have_content label.title
+            expect(page).to have_content label2.title
+          end
+        end
+      end
+    end
+  end
+
+  context 'forked project' do
+    before do
+      fork_project.team << [user, :master]
+      login_as(user)
+    end
+
+    context 'new merge request' do
+      before do
+        visit new_namespace_project_merge_request_path(
+          fork_project.namespace,
+          fork_project,
+          merge_request: {
+            source_project_id: fork_project.id,
+            target_project_id: project.id,
+            source_branch: 'fix',
+            target_branch: 'master'
+          })
+      end
+
+      it 'creates new merge request' do
+        click_button 'Assignee'
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+        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
+        end
+
+        click_button 'Milestone'
+        page.within '.issue-milestone' do
+          click_link milestone.title
+        end
+        expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+        page.within '.js-milestone-select' do
+          expect(page).to have_content milestone.title
+        end
+
+        click_button 'Labels'
+        page.within '.dropdown-menu-labels' do
+          click_link label.title
+          click_link label2.title
+        end
+        page.within '.js-label-select' do
+          expect(page).to have_content label.title
+        end
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+        click_button 'Submit merge request'
+
+        page.within '.issuable-sidebar' do
+          page.within '.assignee' do
+            expect(page).to have_content user.name
+          end
+
+          page.within '.milestone' do
+            expect(page).to have_content milestone.title
+          end
+
+          page.within '.labels' do
+            expect(page).to have_content label.title
+            expect(page).to have_content label2.title
+          end
+        end
+      end
+    end
+
+    context 'edit merge request' do
+      before do
+        merge_request = create(:merge_request,
+                                 source_project: fork_project,
+                                 target_project: project,
+                                 source_branch: 'fix',
+                                 target_branch: 'master'
+                              )
+
+        visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+      end
+
+      it 'should update merge request' do
+        click_button 'Assignee'
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+        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
+        end
+
+        click_button 'Milestone'
+        page.within '.issue-milestone' do
+          click_link milestone.title
+        end
+        expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+        page.within '.js-milestone-select' do
+          expect(page).to have_content milestone.title
+        end
+
+        click_button 'Labels'
+        page.within '.dropdown-menu-labels' do
+          click_link label.title
+          click_link label2.title
+        end
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+        expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+        page.within '.js-label-select' do
+          expect(page).to have_content label.title
+        end
+
+        click_button 'Save changes'
+
+        page.within '.issuable-sidebar' do
+          page.within '.assignee' do
+            expect(page).to have_content user.name
+          end
+
+          page.within '.milestone' do
+            expect(page).to have_content milestone.title
+          end
+
+          page.within '.labels' do
+            expect(page).to have_content label.title
+            expect(page).to have_content label2.title
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..23cee891bace5aa726ab326c3f047c885cc6127c
--- /dev/null
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+feature 'Merge Request versions', js: true, feature: true do
+  let(:merge_request) { create(:merge_request, importing: true) }
+  let(:project) { merge_request.source_project }
+
+  before do
+    login_as :admin
+    merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+    merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+    visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+  end
+
+  it 'show the latest version of the diff' do
+    page.within '.mr-version-dropdown' do
+      expect(page).to have_content 'latest version'
+    end
+
+    expect(page).to have_content '8 changed files'
+  end
+
+  describe 'switch between versions' do
+    before do
+      page.within '.mr-version-dropdown' do
+        find('.btn-default').click
+        click_link 'version 1'
+      end
+    end
+
+    it 'should show older version' do
+      page.within '.mr-version-dropdown' do
+        expect(page).to have_content 'version 1'
+      end
+
+      expect(page).to have_content '5 changed files'
+    end
+
+    it 'show the message about disabled comments' do
+      expect(page).to have_content 'Comments are disabled'
+    end
+  end
+
+  describe 'compare with older version' do
+    before do
+      page.within '.mr-version-compare-dropdown' do
+        find('.btn-default').click
+        click_link 'version 1'
+      end
+    end
+
+    it 'has a path with comparison context' do
+      expect(page).to have_current_path diffs_namespace_project_merge_request_path(
+        project.namespace,
+        project,
+        merge_request.iid,
+        diff_id: 2,
+        start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+      )
+    end
+
+    it 'should have correct value in the compare dropdown' do
+      page.within '.mr-version-compare-dropdown' do
+        expect(page).to have_content 'version 1'
+      end
+    end
+
+    it 'show the message about disabled comments' do
+      expect(page).to have_content 'Comments are disabled'
+    end
+
+    it 'show diff between new and old version' do
+      expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
+    end
+
+    it 'should return to latest version when "Show latest version" button is clicked' do
+      click_link 'Show latest version'
+      page.within '.mr-version-dropdown' do
+        expect(page).to have_content 'latest version'
+      end
+      expect(page).to have_content '8 changed files'
+    end
+  end
+end
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index 60bc07bd1a0fa9218a847b4e566e2883a56cb6cd..c3c3ab33872abc1cd8f9c13c30a20b1206f4e93b 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -2,18 +2,26 @@ require 'spec_helper'
 
 feature 'Merge When Build Succeeds', feature: true, js: true do
   let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
 
-  let(:project)       { create(:project, :public) }
-  let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+  let(:merge_request) do
+    create(:merge_request_with_diffs, source_project: project,
+                                      author: user,
+                                      title: 'Bug NS-04')
+  end
 
-  before do
-    project.team << [user, :master]
-    project.enable_ci
+  let(:pipeline) do
+    create(:ci_pipeline, project: project,
+                         sha: merge_request.diff_head_sha,
+                         ref: merge_request.source_branch)
   end
 
-  context "Active build for Merge Request" do
-    let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
-    let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+  before { project.team << [user, :master] }
+
+  context 'when there is active build for merge request' do
+    background do
+      create(:ci_build, pipeline: pipeline)
+    end
 
     before do
       login_as user
@@ -41,26 +49,30 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
     end
   end
 
-  context 'When it is enabled' do
+  context 'when merge when build succeeds is enabled' do
     let(:merge_request) do
-      create(:merge_request_with_diffs, :simple,  source_project: project, author: user,
-                                                  merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
+      create(:merge_request_with_diffs, :simple,  source_project: project,
+                                                  author: user,
+                                                  merge_user: user,
+                                                  title: 'MepMep',
+                                                  merge_when_build_succeeds: true)
     end
 
-    let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
-    let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
+    let!(:build) do
+      create(:ci_build, pipeline: pipeline)
+    end
 
     before do
       login_as user
       visit_merge_request(merge_request)
     end
 
-    it 'cancels the automatic merge' do
+    it 'allows to cancel the automatic merge' do
       click_link "Cancel Automatic Merge"
 
       expect(page).to have_button "Merge When Build Succeeds"
 
-      visit_merge_request(merge_request) # Needed to refresh the page
+      visit_merge_request(merge_request) # refresh the page
       expect(page).to have_content "Canceled the automatic merge"
     end
 
@@ -70,15 +82,26 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
       click_link "Remove Source Branch When Merged"
       expect(page).to have_content "The source branch will be removed"
     end
+
+    context 'when build succeeds' do
+      background { build.success }
+
+      it 'merges merge request' do
+        visit_merge_request(merge_request) # refresh the page
+
+        expect(page).to have_content 'The changes were merged'
+        expect(merge_request.reload).to be_merged
+      end
+    end
   end
 
-  context 'Build is not active' do
-    it "does not allow for enabling" do
+  context 'when build is not active' do
+    it "does not allow to enable merge when build succeeds" do
       visit_merge_request(merge_request)
       expect(page).not_to have_link "Merge When Build Succeeds"
     end
   end
-
+  
   def visit_merge_request(merge_request)
     visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
   end
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9c4c05252672139120cc9abe777cf02c4ecf6f8e
--- /dev/null
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Pipelines for Merge Requests', feature: true, js: true do
+  include WaitForAjax
+
+  given(:user) { create(:user) }
+  given(:merge_request) { create(:merge_request) }
+  given(:project) { merge_request.target_project }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+  end
+
+  context 'with pipelines' do
+    let!(:pipeline) do
+      create(:ci_empty_pipeline,
+             project: merge_request.source_project,
+             ref: merge_request.source_branch,
+             sha: merge_request.diff_head_sha)
+    end
+
+    before do
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    scenario 'user visits merge request pipelines tab' do
+      page.within('.merge-request-tabs') do
+        click_link('Pipelines')
+      end
+      wait_for_ajax
+
+      expect(page).to have_selector('.pipeline-actions')
+    end
+  end
+
+  context 'without pipelines' do
+    before do
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    scenario 'user visits merge request page' do
+      page.within('.merge-request-tabs') do
+        expect(page).to have_no_link('Pipelines')
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b56fdfe56113e6bd62a82c48ece4ff946271bac7
--- /dev/null
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -0,0 +1,132 @@
+require 'rails_helper'
+
+feature 'Multiple merge requests updating from merge_requests#index', feature: true do
+  include WaitForAjax
+
+  let!(:user)    { create(:user)}
+  let!(:project) { create(:project) }
+  let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+  end
+
+  context 'status', js: true do
+    describe 'close merge request' do
+      before do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+      end
+
+      it 'closes merge request' do
+        change_status('Closed')
+
+        expect(page).to have_selector('.merge-request', count: 0)
+      end
+    end
+
+    describe 'reopen merge request' do
+      before do
+        merge_request.close
+        visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed')
+      end
+
+      it 'reopens merge request' do
+        change_status('Open')
+
+        expect(page).to have_selector('.merge-request', count: 0)
+      end
+    end
+  end
+
+  context 'assignee', js: true do
+    describe 'set assignee' do
+      before do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+      end
+
+      it "updates merge request with assignee" do
+        change_assignee(user.name)
+
+        page.within('.merge-request .controls') do
+          expect(find('.author_link')["title"]).to have_content(user.name)
+        end
+      end
+    end
+
+    describe 'remove assignee' do
+      before do
+        merge_request.assignee = user
+        merge_request.save
+        visit namespace_project_merge_requests_path(project.namespace, project)
+      end
+
+      it "removes assignee from the merge request" do
+        change_assignee('Unassigned')
+
+        expect(find('.merge-request .controls')).not_to have_css('.author_link')
+      end
+    end
+  end
+
+  context 'milestone', js: true do
+    let(:milestone)  { create(:milestone, project: project) }
+
+    describe 'set milestone' do
+      before do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+      end
+
+      it "updates merge request with milestone" do
+        change_milestone(milestone.title)
+
+        expect(find('.merge-request')).to have_content milestone.title
+      end
+    end
+
+    describe 'unset milestone' do
+      before do
+        merge_request.milestone = milestone
+        merge_request.save
+        visit namespace_project_merge_requests_path(project.namespace, project)
+      end
+
+      it "removes milestone from the merge request" do
+        change_milestone("No Milestone")
+
+        expect(find('.merge-request')).not_to have_content milestone.title
+      end
+    end
+  end
+
+  def change_status(text)
+    find('#check_all_issues').click
+    find('.js-issue-status').click
+    find('.dropdown-menu-status a', text: text).click
+    click_update_merge_requests_button
+  end
+
+  def change_assignee(text)
+    find('#check_all_issues').click
+    find('.js-update-assignee').click
+    wait_for_ajax
+
+    page.within '.dropdown-menu-user' do
+      click_link text
+    end
+
+    click_update_merge_requests_button
+  end
+
+  def change_milestone(text)
+    find('#check_all_issues').click
+    find('.issues_bulk_update .js-milestone-select').click
+    find('.dropdown-menu-milestone a', text: text).click
+    click_update_merge_requests_button
+  end
+
+  def click_update_merge_requests_button
+    find('.update_selected_issues').click
+    wait_for_ajax
+  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
new file mode 100644
index 0000000000000000000000000000000000000000..7b8af555f0e3bf7ede508646cd90e9caef3bab6d
--- /dev/null
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -0,0 +1,79 @@
+require 'rails_helper'
+
+feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+  include SlashCommandsHelpers
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+  let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+
+  it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
+    let(:issuable) { create(:merge_request, source_project: project) }
+    let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+  end
+
+  describe 'merge-request-only commands' do
+    before do
+      project.team << [user, :master]
+      login_with(user)
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    after do
+      wait_for_ajax
+    end
+
+    describe 'toggling the WIP prefix in the title from note' do
+      context 'when the current user can toggle the WIP prefix' do
+        it 'adds the WIP: prefix to the title' do
+          write_note("/wip")
+
+          expect(page).not_to have_content '/wip'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          expect(merge_request.reload.work_in_progress?).to eq true
+        end
+
+        it 'removes the WIP: prefix from the title' do
+          merge_request.title = merge_request.wip_title
+          merge_request.save
+          write_note("/wip")
+
+          expect(page).not_to have_content '/wip'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          expect(merge_request.reload.work_in_progress?).to eq false
+        end
+      end
+
+      context 'when the current user cannot toggle the WIP prefix' do
+        let(:guest) { create(:user) }
+        before do
+          project.team << [guest, :guest]
+          logout
+          login_with(guest)
+          visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+        end
+
+        it 'does not change the WIP prefix' do
+          write_note("/wip")
+
+          expect(page).not_to have_content '/wip'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          expect(merge_request.reload.work_in_progress?).to eq false
+        end
+      end
+    end
+
+    describe 'adding a due date from note' do
+      it 'does not recognize the command nor create a note' do
+        write_note('/due 2016-08-28')
+
+        expect(page).not_to have_content '/due 2016-08-28'
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6676821b8077b9d7cfe9887bcf75858675cc4f66
--- /dev/null
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+feature 'Widget Deployments Header', feature: true, js: true do
+  include WaitForAjax
+
+  describe 'when deployed to an environment' do
+    given(:user) { create(:user) }
+    given(:project) { merge_request.target_project }
+    given(:merge_request) { create(:merge_request, :merged) }
+    given(:environment) { create(:environment, project: project) }
+    given(:role) { :developer }
+    given(:sha) { project.commit('master').id }
+    given!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+    given!(:manual) { }
+
+    background do
+      login_as(user)
+      project.team << [user, role]
+      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+    end
+
+    scenario 'displays that the environment is deployed' do
+      wait_for_ajax
+
+      expect(page).to have_content("Deployed to #{environment.name}")
+      expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+    end
+
+    context 'with stop action' do
+      given(:pipeline) { create(:ci_pipeline, project: project) }
+      given(:build) { create(:ci_build, pipeline: pipeline) }
+      given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+      given(:deployment) do
+        create(:deployment, environment: environment, ref: merge_request.target_branch,
+                            sha: sha, deployable: build, on_stop: 'close_app')
+      end
+
+      background do
+        wait_for_ajax
+      end
+
+      scenario 'does show stop button' do
+        expect(page).to have_link('Stop environment')
+      end
+
+      scenario 'does start build when stop button clicked' do
+        click_link('Stop environment')
+
+        expect(page).to have_content('close_app')
+      end
+
+      context 'for reporter' do
+        given(:role) { :reporter }
+
+        scenario 'does not show stop button' do
+          expect(page).not_to have_link('Stop environment')
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c43661e56813ae8a9135266d5c6a57abd3578de6..b8c838bf7ab0fee8ccc7a11436e3372dbaa1966c 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -3,9 +3,8 @@ require 'rails_helper'
 feature 'Milestone', feature: true do
   include WaitForAjax
 
-  let(:project) { create(:project, :public) }
+  let(:project) { create(:empty_project, :public) }
   let(:user)   { create(:user) }
-  let(:milestone) { create(:milestone, project: project, title: 8.7) }
 
   before do
     project.team << [user, :master]
@@ -13,7 +12,7 @@ feature 'Milestone', feature: true do
   end
 
   feature 'Create a milestone' do
-    scenario 'shows an informative message for a new issue' do
+    scenario 'shows an informative message for a new milestone' do
       visit new_namespace_project_milestone_path(project.namespace, project)
       page.within '.milestone-form' do
         fill_in "milestone_title", with: '8.7'
@@ -26,10 +25,26 @@ feature 'Milestone', feature: true do
 
   feature 'Open a milestone with closed issues' do
     scenario 'shows an informative message' do
+      milestone = create(:milestone, project: project, title: 8.7)
+
       create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
       visit namespace_project_milestone_path(project.namespace, project, milestone)
 
       expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.')
     end
   end
+
+  feature 'Open a milestone with an existing title' do
+    scenario 'displays validation message' do
+      milestone = create(:milestone, project: project, title: 8.7)
+
+      visit new_namespace_project_milestone_path(project.namespace, project)
+      page.within '.milestone-form' do
+        fill_in "milestone_title", with: milestone.title
+      end
+      find('input[name="commit"]').click
+
+      expect(find('.alert-danger')).to have_content('Title has already been taken')
+    end
+  end
 end
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b603f51545bbce278082ca7bdb25a4b64ac6eea
--- /dev/null
+++ b/spec/features/milestones/milestones_spec.rb
@@ -0,0 +1,86 @@
+require 'rails_helper'
+
+describe 'Milestone draggable', feature: true, js: true do
+  let(:milestone) { create(:milestone, project: project, title: 8.14) }
+  let(:project)   { create(:empty_project, :public) }
+  let(:user)      { create(:user) }
+
+  context 'issues' do
+    let(:issue)        { page.find_by_id('issues-list-unassigned').find('li') }
+    let(:issue_target) { page.find_by_id('issues-list-ongoing') }
+
+    it 'does not allow guest to drag issue' do
+      create_and_drag_issue
+
+      expect(issue_target).not_to have_selector('.issuable-row')
+    end
+
+    it 'does not allow authorized user to drag issue' do
+      login_as(user)
+      create_and_drag_issue
+
+      expect(issue_target).not_to have_selector('.issuable-row')
+    end
+
+    it 'allows author to drag issue' do
+      login_as(user)
+      create_and_drag_issue(author: user)
+
+      expect(issue_target).to have_selector('.issuable-row')
+    end
+
+    it 'allows admin to drag issue' do
+      login_as(:admin)
+      create_and_drag_issue
+
+      expect(issue_target).to have_selector('.issuable-row')
+    end
+  end
+
+  context 'merge requests' do
+    let(:merge_request)        { page.find_by_id('merge_requests-list-unassigned').find('li') }
+    let(:merge_request_target) { page.find_by_id('merge_requests-list-ongoing') }
+
+    it 'does not allow guest to drag merge request' do
+      create_and_drag_merge_request
+
+      expect(merge_request_target).not_to have_selector('.issuable-row')
+    end
+
+    it 'does not allow authorized user to drag merge request' do
+      login_as(user)
+      create_and_drag_merge_request
+
+      expect(merge_request_target).not_to have_selector('.issuable-row')
+    end
+
+    it 'allows author to drag merge request' do
+      login_as(user)
+      create_and_drag_merge_request(author: user)
+
+      expect(merge_request_target).to have_selector('.issuable-row')
+    end
+
+    it 'allows admin to drag merge request' do
+      login_as(:admin)
+      create_and_drag_merge_request
+
+      expect(merge_request_target).to have_selector('.issuable-row')
+    end
+  end
+
+  def create_and_drag_issue(params = {})
+    create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
+
+    visit namespace_project_milestone_path(project.namespace, project, milestone)
+    issue.drag_to(issue_target)
+  end
+
+  def create_and_drag_merge_request(params = {})
+    create(:merge_request, params.merge(title: 'Foo', source_project: project, target_project: project, milestone: milestone))
+
+    visit namespace_project_milestone_path(project.namespace, project, milestone)
+    page.find("a[href='#tab-merge-requests']").click
+    merge_request.drag_to(merge_request_target)
+  end
+end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 7a9edbbe33968fda188cd4f9772f80b9013181c2..5d7247e2a626bc3808abd2317c04394279ff6223 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -141,7 +141,7 @@ describe 'Comments', feature: true do
     let(:project2) { create(:project, :private) }
     let(:issue) { create(:issue, project: project2) }
     let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
-    let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
+    let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") }
 
     it 'shows the system note' do
       login_as :admin
@@ -240,6 +240,18 @@ describe 'Comments', feature: true do
           is_expected.to have_css('.notes_holder .note', count: 1)
           is_expected.to have_button('Reply...')
         end
+
+        it 'adds code to discussion' do
+          click_button 'Reply...'
+
+          page.within(first('.js-discussion-note-form')) do
+            fill_in 'note[note]', with: '```{{ test }}```'
+
+            click_button('Comment')
+          end
+
+          expect(page).to have_content('{{ test }}')
+        end
       end
     end
   end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c3d8c349ca4c1bfeae0526a1a52383814a111932..7a562b5e03d079c4559c9df83136f8967a497320 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -32,4 +32,33 @@ describe 'Profile account page', feature: true do
       expect(current_path).to eq(profile_account_path)
     end
   end
+
+  describe 'when I reset private token' do
+    before do
+      visit profile_account_path
+    end
+
+    it 'resets private token' do
+      previous_token = find("#private-token").value
+
+      click_link('Reset private token')
+
+      expect(find('#private-token').value).not_to eq(previous_token)
+    end
+  end
+
+  describe 'when I reset incoming email token' do
+    before do
+      allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+      visit profile_account_path
+    end
+
+    it 'resets incoming email token' do
+      previous_token = find('#incoming-email-token').value
+
+      click_link('Reset incoming email token')
+
+      expect(find('#incoming-email-token').value).not_to eq(previous_token)
+    end
+  end
 end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb1050d21c6665e5c2c203ac210dd56b9514be71
--- /dev/null
+++ b/spec/features/profiles/keys_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+feature 'Profile > SSH Keys', feature: true do
+  let(:user) { create(:user) }
+
+  before do
+    login_as(user)
+  end
+
+  describe 'User adds a key' do
+    before do
+      visit profile_keys_path
+    end
+
+    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'
+    end
+
+    scenario 'saves the new key' do
+      attrs = attributes_for(:key)
+
+      fill_in('Key', with: attrs[:key])
+      fill_in('Title', with: attrs[:title])
+      click_button('Add key')
+
+      expect(page).to have_content("Title: #{attrs[:title]}")
+      expect(page).to have_content(attrs[:key])
+    end
+  end
+
+  scenario 'User sees their keys' do
+    key = create(:key, user: user)
+    visit profile_keys_path
+
+    expect(page).to have_content(key.title)
+  end
+
+  scenario 'User removes a key via the key index' do
+    create(:key, user: user)
+    visit profile_keys_path
+
+    click_link('Remove')
+
+    expect(page).to have_content('Your SSH keys (0)')
+  end
+
+  scenario 'User removes a key via its details page' do
+    key = create(:key, user: user)
+    visit profile_key_path(key)
+
+    click_link('Remove')
+
+    expect(page).to have_content('Your SSH keys (0)')
+  end
+end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index af86d3c338a8e9db156affb048b89cb0bb411823..01a95bf49acb465ec9d49243214f46bf7a685409 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -4,12 +4,6 @@ feature 'test coverage badge' do
   given!(:user) { create(:user) }
   given!(:project) { create(:project, :private) }
 
-  given!(:pipeline) do
-    create(:ci_pipeline, project: project,
-                         ref: 'master',
-                         sha: project.commit.id)
-  end
-
   context 'when user has access to view badge' do
     background do
       project.team << [user, :developer]
@@ -17,8 +11,10 @@ feature 'test coverage badge' do
     end
 
     scenario 'user requests coverage badge image for pipeline' do
-      create_job(coverage: 100, name: 'test:1')
-      create_job(coverage: 90, name: 'test:2')
+      create_pipeline do |pipeline|
+        create_build(pipeline, coverage: 100, name: 'test:1')
+        create_build(pipeline, coverage: 90, name: 'test:2')
+      end
 
       show_test_coverage_badge
 
@@ -26,9 +22,11 @@ feature 'test coverage badge' do
     end
 
     scenario 'user requests coverage badge for specific job' do
-      create_job(coverage: 50, name: 'test:1')
-      create_job(coverage: 50, name: 'test:2')
-      create_job(coverage: 85, name: 'coverage')
+      create_pipeline do |pipeline|
+        create_build(pipeline, coverage: 50, name: 'test:1')
+        create_build(pipeline, coverage: 50, name: 'test:2')
+        create_build(pipeline, coverage: 85, name: 'coverage')
+      end
 
       show_test_coverage_badge(job: 'coverage')
 
@@ -36,7 +34,9 @@ feature 'test coverage badge' do
     end
 
     scenario 'user requests coverage badge for pipeline without coverage' do
-      create_job(coverage: nil, name: 'test')
+      create_pipeline do |pipeline|
+        create_build(pipeline, coverage: nil, name: 'test')
+      end
 
       show_test_coverage_badge
 
@@ -54,10 +54,19 @@ feature 'test coverage badge' do
     end
   end
 
-  def create_job(coverage:, name:)
-    create(:ci_build, name: name,
-                      coverage: coverage,
-                      pipeline: pipeline)
+  def create_pipeline
+    opts = { project: project, ref: 'master', sha: project.commit.id }
+
+    create(:ci_pipeline, opts).tap do |pipeline|
+      yield pipeline
+      pipeline.update_status
+    end
+  end
+
+  def create_build(pipeline, coverage:, name:)
+    opts = { pipeline: pipeline, coverage: coverage, name: name }
+
+    create(:ci_build, :success, opts)
   end
 
   def show_test_coverage_badge(job: nil)
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..92028c1936102665d38529103de74ad3c5eda124
--- /dev/null
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Download buttons in branches page', feature: true do
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:status) { 'success' }
+  given(:project) { create(:project) }
+
+  given(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit('binary-encoding').sha,
+           ref: 'binary-encoding', # make sure the branch is in the 1st page!
+           status: status)
+  end
+
+  given!(:build) do
+    create(:ci_build, :success, :artifacts,
+           pipeline: pipeline,
+           status: pipeline.status,
+           name: 'build')
+  end
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+  end
+
+  describe 'when checking branches' do
+    context 'with artifacts' do
+      before do
+        visit namespace_project_branches_path(project.namespace, project)
+      end
+
+      scenario 'shows download artifacts button' do
+        href = latest_succeeded_namespace_project_artifacts_path(
+          project.namespace, project, 'binary-encoding/download',
+          job: 'build')
+
+        expect(page).to have_link "Download '#{build.name}'", href: href
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79abba21854309b77ba86fadfdc721d4d636e1f0..d26a0caf0368e3f10b26bb8a6080b7304c3fcf33 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -1,32 +1,46 @@
 require 'spec_helper'
 
 describe 'Branches', feature: true do
-  let(:project) { create(:project) }
+  let(:project) { create(:project, :public) }
   let(:repository) { project.repository }
 
-  before do
-    login_as :user
-    project.team << [@user, :developer]
-  end
+  context 'logged in' do
+    before do
+      login_as :user
+      project.team << [@user, :developer]
+    end
 
-  describe 'Initial branches page' do
-    it 'shows all the branches' do
-      visit namespace_project_branches_path(project.namespace, project)
+    describe 'Initial branches page' do
+      it 'shows all the branches' do
+        visit namespace_project_branches_path(project.namespace, project)
 
-      repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
-      expect(page).to have_content("Protected branches can be managed in project settings")
+        repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
+        expect(page).to have_content("Protected branches can be managed in project settings")
+      end
     end
-  end
 
-  describe 'Find branches' do
-    it 'shows filtered branches', js: true do
-      visit namespace_project_branches_path(project.namespace, project, project.id)
+    describe 'Find branches' do
+      it 'shows filtered branches', js: true do
+        visit namespace_project_branches_path(project.namespace, project)
+
+        fill_in 'branch-search', with: 'fix'
+        find('#branch-search').native.send_keys(:enter)
 
-      fill_in 'branch-search', with: 'fix'
-      find('#branch-search').native.send_keys(:enter)
+        expect(page).to have_content('fix')
+        expect(find('.all-branches')).to have_selector('li', count: 1)
+      end
+    end
+  end
+
+  context 'logged out' do
+    before do
+      visit namespace_project_branches_path(project.namespace, project)
+    end
 
-      expect(page).to have_content('fix')
-      expect(find('.all-branches')).to have_selector('li', count: 1)
+    it 'does not show merge request button' do
+      page.within first('.all-branches li') do
+        expect(page).not_to have_content 'Merge Request'
+      end
     end
   end
 end
diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb
similarity index 60%
rename from spec/features/builds_spec.rb
rename to spec/features/projects/builds_spec.rb
index 0cfeb2e57d8ebd32ef8cb8bec9b571b6f1b4e626..a8022a5361f9e25b971618fbc177c2c90e69a0c1 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -1,4 +1,5 @@
 require 'spec_helper'
+require 'tempfile'
 
 describe "Builds" do
   let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
@@ -6,7 +7,7 @@ describe "Builds" do
   before do
     login_as(:user)
     @commit = FactoryGirl.create :ci_pipeline
-    @build = FactoryGirl.create :ci_build, pipeline: @commit
+    @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit
     @build2 = FactoryGirl.create :ci_build
     @project = @commit.project
     @project.team << [@user, :developer]
@@ -78,12 +79,14 @@ describe "Builds" do
       click_link "Cancel running"
     end
 
-    it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
-    it { expect(page).to have_content 'canceled' }
-    it { expect(page).to have_content @build.short_sha }
-    it { expect(page).to have_content @build.ref }
-    it { expect(page).to have_content @build.name }
-    it { expect(page).not_to have_link 'Cancel running' }
+    it 'shows all necessary content' do
+      expect(page).to have_selector('.nav-links li.active', text: 'All')
+      expect(page).to have_content 'canceled'
+      expect(page).to have_content @build.short_sha
+      expect(page).to have_content @build.ref
+      expect(page).to have_content @build.name
+      expect(page).not_to have_link 'Cancel running'
+    end
   end
 
   describe "GET /:project/builds/:id" do
@@ -92,10 +95,12 @@ describe "Builds" do
         visit namespace_project_build_path(@project.namespace, @project, @build)
       end
 
-      it { expect(page.status_code).to eq(200) }
-      it { expect(page).to have_content @commit.sha[0..7] }
-      it { expect(page).to have_content @commit.git_commit_message }
-      it { expect(page).to have_content @commit.git_author_name }
+      it 'shows commit`s data' do
+        expect(page.status_code).to eq(200)
+        expect(page).to have_content @commit.sha[0..7]
+        expect(page).to have_content @commit.git_commit_message
+        expect(page).to have_content @commit.git_author_name
+      end
     end
 
     context "Build from other project" do
@@ -156,7 +161,6 @@ describe "Builds" do
     context 'Build raw trace' do
       before do
         @build.run!
-        @build.trace = 'BUILD TRACE'
         visit namespace_project_build_path(@project.namespace, @project, @build)
       end
 
@@ -164,6 +168,26 @@ describe "Builds" do
         expect(page).to have_link 'Raw'
       end
     end
+
+    describe 'Variables' do
+      before do
+        @trigger_request = create :ci_trigger_request_with_variables
+        @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request
+        visit namespace_project_build_path(@project.namespace, @project, @build)
+      end
+
+      it 'shows variable key and value after click', js: true do
+        expect(page).to have_css('.reveal-variables')
+        expect(page).not_to have_css('.js-build-variable')
+        expect(page).not_to have_css('.js-build-value')
+
+        click_button 'Reveal Variables'
+
+        expect(page).not_to have_css('.reveal-variables')
+        expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+        expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+      end
+    end
   end
 
   describe "POST /:project/builds/:id/cancel" do
@@ -174,9 +198,11 @@ describe "Builds" do
         click_link "Cancel"
       end
 
-      it { expect(page.status_code).to eq(200) }
-      it { expect(page).to have_content 'canceled' }
-      it { expect(page).to have_content 'Retry' }
+      it 'loads the page and shows all needed controls' do
+        expect(page.status_code).to eq(200)
+        expect(page).to have_content 'canceled'
+        expect(page).to have_content 'Retry'
+      end
     end
 
     context "Build from other project" do
@@ -196,7 +222,9 @@ describe "Builds" do
         @build.run!
         visit namespace_project_build_path(@project.namespace, @project, @build)
         click_link 'Cancel'
-        click_link 'Retry'
+        page.within('.build-header') do
+          click_link 'Retry build'
+        end
       end
 
       it 'shows the right status and buttons' do
@@ -255,35 +283,101 @@ describe "Builds" do
     end
   end
 
-  describe "GET /:project/builds/:id/raw" do
-    context "Build from project" do
-      before do
-        Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
-        @build.run!
-        @build.trace = 'BUILD TRACE'
-        visit namespace_project_build_path(@project.namespace, @project, @build)
-        page.within('.js-build-sidebar') { click_link 'Raw' }
+  describe 'GET /:project/builds/:id/raw' do
+    context 'access source' do
+      context 'build from project' do
+        before do
+          Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+          @build.run!
+          visit namespace_project_build_path(@project.namespace, @project, @build)
+          page.within('.js-build-sidebar') { click_link 'Raw' }
+        end
+
+        it 'sends the right headers' do
+          expect(page.status_code).to eq(200)
+          expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+          expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+        end
       end
 
-      it 'sends the right headers' do
-        expect(page.status_code).to eq(200)
-        expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
-        expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+      context 'build from other project' do
+        before do
+          Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+          @build2.run!
+          visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+        end
+
+        it 'sends the right headers' do
+          expect(page.status_code).to eq(404)
+        end
       end
     end
 
-    context "Build from other project" do
-      before do
-        Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
-        @build2.run!
-        @build2.trace = 'BUILD TRACE'
-        visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
-        puts page.status_code
-        puts current_url
+    context 'storage form' do
+      let(:existing_file) { Tempfile.new('existing-trace-file').path }
+      let(:non_existing_file) do
+        file = Tempfile.new('non-existing-trace-file')
+        path = file.path
+        file.unlink
+        path
+      end
+
+      context 'when build has trace in file' do
+        before do
+          Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+          @build.run!
+          visit namespace_project_build_path(@project.namespace, @project, @build)
+
+          allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+          allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file)
+          allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+          page.within('.js-build-sidebar') { click_link 'Raw' }
+        end
+
+        it 'sends the right headers' do
+          expect(page.status_code).to eq(200)
+          expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+          expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+        end
       end
 
-      it 'sends the right headers' do
-        expect(page.status_code).to eq(404)
+      context 'when build has trace in old file' do
+        before do
+          Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+          @build.run!
+          visit namespace_project_build_path(@project.namespace, @project, @build)
+
+          allow_any_instance_of(Project).to receive(:ci_id).and_return(999)
+          allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+          allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file)
+
+          page.within('.js-build-sidebar') { click_link 'Raw' }
+        end
+
+        it 'sends the right headers' do
+          expect(page.status_code).to eq(200)
+          expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+          expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+        end
+      end
+
+      context 'when build has trace in DB' do
+        before do
+          Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+          @build.run!
+          visit namespace_project_build_path(@project.namespace, @project, @build)
+
+          allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+          allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+          allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+          page.within('.js-build-sidebar') { click_link 'Raw' }
+        end
+
+        it 'sends the right headers' do
+          expect(page.status_code).to eq(404)
+        end
       end
     end
   end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 1b4ff6b6f1b15c4c13213bca3c9532e875027036..e45e3a36d015e1326479e49d870039d47fb2e57c 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -1,4 +1,5 @@
 require 'spec_helper'
+include WaitForAjax
 
 describe 'Cherry-pick Commits' do
   let(:project) { create(:project) }
@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
   before do
     login_as :user
     project.team << [@user, :master]
-    visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+    visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
   end
 
   context "I cherry-pick a commit" do
     it do
-      visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
       find("a[href='#modal-cherry-pick-commit']").click
       expect(page).not_to have_content('v1.0.0') # Only branches, not tags
       page.within('#modal-cherry-pick-commit') do
@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
 
   context "I cherry-pick a merge commit" do
     it do
-      visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
       find("a[href='#modal-cherry-pick-commit']").click
       page.within('#modal-cherry-pick-commit') do
         uncheck 'create_merge_request'
@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
 
   context "I cherry-pick a commit that was previously cherry-picked" do
     it do
-      visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
       find("a[href='#modal-cherry-pick-commit']").click
       page.within('#modal-cherry-pick-commit') do
         uncheck 'create_merge_request'
@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
 
   context "I cherry-pick a commit in a new merge request" do
     it do
-      visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
       find("a[href='#modal-cherry-pick-commit']").click
       page.within('#modal-cherry-pick-commit') do
         click_button 'Cherry-pick'
@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
       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.')
     end
   end
+
+  context "I cherry-pick a commit from a different branch", js: true do
+    it do
+      find('.commit-action-buttons a.dropdown-toggle').click
+      find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+      page.within('#modal-cherry-pick-commit') do
+        click_button 'master'
+      end
+
+      wait_for_ajax
+
+      page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+        click_link 'feature'
+      end
+
+      page.within('#modal-cherry-pick-commit') do
+        uncheck 'create_merge_request'
+        click_button 'Cherry-pick'
+      end
+
+      expect(page).to have_content('The commit has been successfully cherry-picked.')
+    end
+  end
 end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1643fd1f436de0ac68d7540c95c5a03713d3177
--- /dev/null
+++ b/spec/features/projects/edit_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+feature 'Project edit', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user)    { create(:user) }
+  let(:project) { create(:project) }
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+
+    visit edit_namespace_project_path(project.namespace, project)
+  end
+
+  context 'feature visibility' do
+    context 'merge requests select' do
+      it 'hides merge requests section' do
+        select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+        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'
+
+        wait_for_ajax
+
+        expect(page).to have_selector('.merge-requests-feature', visible: false)
+      end
+    end
+
+    context 'builds select' do
+      it 'hides merge requests 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)
+
+        click_button 'Save changes'
+
+        wait_for_ajax
+
+        expect(page).to have_selector('.builds-feature', visible: false)
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09aa6758b5cae5603cc69d39707bd3c4893209fe
--- /dev/null
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+include WaitForAjax
+
+describe 'Edit Project Settings', feature: true do
+  include WaitForAjax
+
+  let(:member) { create(:user) }
+  let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
+  let!(:issue) { create(:issue, project: project) }
+  let(:non_member) { create(:user) }
+
+  describe 'project features visibility selectors', js: true do
+    before do
+      project.team << [member, :master]
+      login_as(member)
+    end
+
+    tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" }
+
+    tools.each do |tool_name, shortcut_name|
+      describe "feature #{tool_name}" do
+        it 'toggles visibility' do
+          visit edit_namespace_project_path(project.namespace, project)
+
+          select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
+          click_button 'Save changes'
+          wait_for_ajax
+          expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
+
+          select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
+          click_button 'Save changes'
+          wait_for_ajax
+          expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+
+          select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
+          click_button 'Save changes'
+          wait_for_ajax
+          expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+
+          sleep 0.1
+        end
+      end
+    end
+
+    context "pipelines subtabs" do
+      it "shows builds when enabled" do
+        visit namespace_project_pipelines_path(project.namespace, project)
+
+        expect(page).to have_selector(".shortcuts-builds")
+      end
+
+      it "hides builds when disabled" do
+        allow(Ability).to receive(:allowed?).with(member, :read_builds, project).and_return(false)
+
+        visit namespace_project_pipelines_path(project.namespace, project)
+
+        expect(page).not_to have_selector(".shortcuts-builds")
+      end
+    end
+  end
+
+  describe 'project features visibility pages' do
+    before do
+      @tools =
+        {
+          builds: namespace_project_pipelines_path(project.namespace, project),
+          issues: namespace_project_issues_path(project.namespace, project),
+          wiki: namespace_project_wiki_path(project.namespace, project, :home),
+          snippets: namespace_project_snippets_path(project.namespace, project),
+          merge_requests: namespace_project_merge_requests_path(project.namespace, project),
+        }
+    end
+
+    context 'normal user' do
+      it 'renders 200 if tool is enabled' do
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
+          visit url
+          expect(page.status_code).to eq(200)
+        end
+      end
+
+      it 'renders 404 if feature is disabled' do
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
+          visit url
+          expect(page.status_code).to eq(404)
+        end
+      end
+
+      it 'renders 404 if feature is enabled only for team members' do
+        project.team.truncate
+
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+          visit url
+          expect(page.status_code).to eq(404)
+        end
+      end
+
+      it 'renders 200 if users is member of group' do
+        group = create(:group)
+        project.group = group
+        project.save
+
+        group.add_owner(member)
+
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+          visit url
+          expect(page.status_code).to eq(200)
+        end
+      end
+    end
+
+    context 'admin user' do
+      before do
+        non_member.update_attribute(:admin, true)
+        login_as(non_member)
+      end
+
+      it 'renders 404 if feature is disabled' do
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
+          visit url
+          expect(page.status_code).to eq(404)
+        end
+      end
+
+      it 'renders 200 if feature is enabled only for team members' do
+        project.team.truncate
+
+        @tools.each do |method_name, url|
+          project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
+          visit url
+          expect(page.status_code).to eq(200)
+        end
+      end
+    end
+  end
+
+  describe 'repository visibility', js: true do
+    before do
+      project.team << [member, :master]
+      login_as(member)
+      visit edit_namespace_project_path(project.namespace, project)
+    end
+
+    it "disables repository related features" do
+      select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+
+      expect(find(".edit-project")).to have_selector("select.disabled", count: 2)
+    end
+
+    it "shows empty features project homepage" do
+      select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+      select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+      select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+
+      click_button "Save changes"
+      wait_for_ajax
+
+      visit namespace_project_path(project.namespace, project)
+
+      expect(page).to have_content "Customize your workflow!"
+    end
+
+    it "hides project activity tabs" do
+      select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+      select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+      select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+
+      click_button "Save changes"
+      wait_for_ajax
+
+      visit activity_namespace_project_path(project.namespace, project)
+
+      page.within(".event-filter") do
+        expect(page).to have_selector("a", count: 2)
+        expect(page).not_to have_content("Push events")
+        expect(page).not_to have_content("Merge events")
+        expect(page).not_to have_content("Comments")
+      end
+    end
+  end
+
+  # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056
+  describe 'project statistic visibility' do
+    let!(:project) { create(:project, :private) }
+
+    before do
+      project.team << [member, :guest]
+      login_as(member)
+      visit namespace_project_path(project.namespace, project)
+    end
+
+    it "does not show project statistic for guest" do
+      expect(page).not_to have_selector('.project-stats')
+    end
+  end
+end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69295e450d01611d6255d69c0eb38b571024f901
--- /dev/null
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'user checks git blame', feature: true do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  before do
+    project.team << [user, :master]
+    login_with(user)
+    visit namespace_project_tree_path(project.namespace, project, project.default_branch)
+  end
+
+  scenario "can see blame of '.gitignore'" do
+    click_link ".gitignore"
+    click_link 'Blame'
+    
+    expect(page).to have_content "*.rb"
+    expect(page).to have_content "Dmitriy Zaporozhets"
+    expect(page).to have_content "Initial commit"
+  end
+end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d7c29a7e07449cc29cbd795f41d52d02ca9f33ac
--- /dev/null
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Download buttons in files tree', feature: true do
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:status) { 'success' }
+  given(:project) { create(:project) }
+
+  given(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit.sha,
+           ref: project.default_branch,
+           status: status)
+  end
+
+  given!(:build) do
+    create(:ci_build, :success, :artifacts,
+           pipeline: pipeline,
+           status: pipeline.status,
+           name: 'build')
+  end
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+  end
+
+  describe 'when files tree' do
+    context 'with artifacts' do
+      before do
+        visit namespace_project_tree_path(
+          project.namespace, project, project.default_branch)
+      end
+
+      scenario 'shows download artifacts button' do
+        href = latest_succeeded_namespace_project_artifacts_path(
+          project.namespace, project, "#{project.default_branch}/download",
+          job: 'build')
+
+        expect(page).to have_link "Download '#{build.name}'", href: href
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..012befa7990a55cef0c5bff40d3a88a044687ffd
--- /dev/null
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'User uses soft wrap whilst editing file', feature: true, js: true do
+  before do
+    user = create(:user)
+    project = create(:project)
+    project.team << [user, :master]
+    login_as user
+    visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'test_file-name')
+    editor = find('.file-editor.code')
+    editor.click
+    editor.send_keys 'Touch water with paw then recoil in horror chase dog then
+      run away chase the pig around the house eat owner\'s food, and knock
+      dish off table head butt cant eat out of my own dish. Cat is love, cat
+      is life rub face on everything poop on grasses so meow. Playing with
+      balls of wool flee in terror at cucumber discovered on floor run in
+      circles tuxedo cats always looking dapper, but attack dog, run away
+      and pretend to be victim so all of a sudden cat goes crazy, yet chase
+      laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+      hanging out of own butt jump off balcony, onto stranger\'s head yet
+      chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+  end
+
+  let(:toggle_button) { find('.soft-wrap-toggle') }
+
+  scenario 'user clicks the "Soft wrap" button and then "No wrap" button' do
+    wrapped_content_width = get_content_width
+    toggle_button.click
+    expect(toggle_button).to have_content 'No wrap'
+    unwrapped_content_width = get_content_width
+    expect(unwrapped_content_width).to be < wrapped_content_width
+
+    toggle_button.click
+    expect(toggle_button).to have_content 'Soft wrap'
+    expect(get_content_width).to be > unwrapped_content_width
+  end
+
+  def get_content_width
+    find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+  end
+end
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fc88fd74af879e53c28b1c895c3d5305146d2239
--- /dev/null
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+feature 'Find file keyboard shortcuts', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  before do
+    project.team << [user, :master]
+    login_as user
+
+    visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref)
+
+    wait_for_ajax
+  end
+
+  it 'opens file when pressing enter key' do
+    fill_in 'file_find', with: 'CHANGELOG'
+
+    find('#file_find').native.send_keys(:enter)
+
+    expect(page).to have_selector('.blob-content-holder')
+
+    page.within('.file-title') do
+      expect(page).to have_content('CHANGELOG')
+    end
+  end
+
+  it 'navigates files with arrow keys' do
+    fill_in 'file_find', with: 'application.'
+
+    find('#file_find').native.send_keys(:down)
+    find('#file_find').native.send_keys(:enter)
+
+    expect(page).to have_selector('.blob-content-holder')
+
+    page.within('.file-title') do
+      expect(page).to have_content('application.js')
+    end
+  end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1921ea6d8aec39c9f0801c6568777ae5fed4a466
--- /dev/null
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'GFM autocomplete loading', feature: true, js: true do
+  let(:project)   { create(:project) }
+
+  before do
+    login_as :admin
+
+    visit namespace_project_path(project.namespace, project)
+  end
+
+  it 'does not load on project#show' do
+    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+  end
+
+  it 'loads on new issue page' do
+    visit new_namespace_project_issue_path(project.namespace, project)
+
+    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+  end
+end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a71a03fbd9fc0f03736af9a7471c716ba9c666e
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+  include Select2Helper
+
+  let(:master) { create(:user) }
+  let(:project) { create(:project) }
+  let!(:group) { create(:group) }
+
+  background do
+    project.team << [master, :master]
+    login_as(master)
+  end
+
+  context 'setting an expiration date for a group link' do
+    before do
+      visit namespace_project_group_links_path(project.namespace, project)
+
+      select2 group.id, from: '#link_group_id'
+      fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+      page.find('body').click
+      click_on 'Share'
+    end
+
+    it 'shows the expiration time with a warning class' do
+      page.within('.enabled-groups') do
+        expect(page).to have_content('expires in 4 days')
+        expect(page).to have_selector('.text-warning')
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c22441f8929bb46bd8ba857dc334c414d9c7da94
--- /dev/null
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Guest navigation menu" do
+  let(:project) { create :empty_project, :private }
+  let(:guest) { create :user }
+
+  before do
+    project.team << [guest, :guest]
+
+    login_as(guest)
+  end
+
+  it "shows allowed tabs only" do
+    visit namespace_project_path(project.namespace, project)
+
+    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
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..52d08982c7accd656756e1855e4ec35324889837
--- /dev/null
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+# Integration test that exports a file using the Import/Export feature
+# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
+# we''l have to either include it adding the model that includes it to the +safe_list+
+# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
+feature 'Import/Export - project export integration test', feature: true, js: true do
+  include Select2Helper
+  include ExportFileHelper
+
+  let(:user) { create(:admin) }
+  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] }
+  let(:safe_list) do
+    {
+      token: [ProjectHook, Ci::Trigger, CommitStatus],
+      key: [Project, Ci::Variable, :yaml_variables]
+    }
+  end
+  let(:safe_hashes) { { yaml_variables: %w[key value public] } }
+
+  let(:project) { setup_project }
+
+  background do
+    allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+  end
+
+  after do
+    FileUtils.rm_rf(export_path, secure: true)
+  end
+
+  context 'admin user' do
+    before do
+      login_as(user)
+    end
+
+    scenario 'exports a project successfully' do
+      visit edit_namespace_project_path(project.namespace, project)
+
+      expect(page).to have_content('Export project')
+
+      click_link 'Export project'
+
+      visit edit_namespace_project_path(project.namespace, project)
+
+      expect(page).to have_content('Download export')
+
+      expect(file_permissions(project.export_path)).to eq(0700)
+
+      in_directory_with_expanded_export(project) do |exit_status, tmpdir|
+        expect(exit_status).to eq(0)
+
+        project_json_path = File.join(tmpdir, 'project.json')
+        expect(File).to exist(project_json_path)
+
+        project_hash = JSON.parse(IO.read(project_json_path))
+
+        sensitive_words.each do |sensitive_word|
+          found = find_sensitive_attributes(sensitive_word, project_hash)
+
+          expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
+        end
+      end
+    end
+
+    def failure_message(key_found, parent, sensitive_word)
+      <<-MSG
+        Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect}
+
+        If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG.
+
+        Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
+        correspondent hash or model as the value.
+
+        IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+        CURRENT_SPEC: #{__FILE__}
+      MSG
+    end
+  end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index f707ccf4e93e0113d153bbcf655db346a34f67f5..3015576f6f8561322936f33ade8be3a77527b4d4 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,15 +1,10 @@
 require 'spec_helper'
 
-feature 'project import', feature: true, js: true do
+feature 'Import/Export - project import integration test', feature: true, js: true do
   include Select2Helper
 
-  let(:admin) { create(:admin) }
-  let(:normal_user) { create(:user) }
-  let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
   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(:project) { Project.last }
-  let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) }
 
   background do
     allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -19,41 +14,43 @@ feature 'project import', feature: true, js: true do
     FileUtils.rm_rf(export_path, secure: true)
   end
 
-  context 'admin user' do
+  context 'when selecting the namespace' do
+    let(:user) { create(:admin) }
+    let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+
     before do
-      login_as(admin)
+      login_as(user)
     end
 
     scenario 'user imports an exported project successfully' do
-      expect(Project.all.count).to be_zero
-
       visit new_project_path
 
-      select2('2', from: '#project_namespace_id')
+      select2(namespace.id, from: '#project_namespace_id')
       fill_in :project_path, with: 'test-project-path', visible: true
       click_link 'GitLab export'
 
       expect(page).to have_content('GitLab project export')
-      expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+      expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
 
       attach_file('file', file)
 
-      click_on 'Import project' # import starts
+      expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1)
 
+      project = Project.last
       expect(project).not_to be_nil
       expect(project.issues).not_to be_empty
       expect(project.merge_requests).not_to be_empty
-      expect(project_hook).to exist
-      expect(wiki_exists?).to be true
+      expect(project_hook_exists?(project)).to be true
+      expect(wiki_exists?(project)).to be true
       expect(project.import_status).to eq('finished')
     end
 
     scenario 'invalid project' do
-      project = create(:project, namespace_id: 2)
+      project = create(:project, namespace: namespace)
 
       visit new_project_path
 
-      select2('2', from: '#project_namespace_id')
+      select2(namespace.id, from: '#project_namespace_id')
       fill_in :project_path, with: project.name, visible: true
       click_link 'GitLab export'
 
@@ -66,11 +63,11 @@ feature 'project import', feature: true, js: true do
     end
 
     scenario 'project with no name' do
-      create(:project, namespace_id: 2)
+      create(:project, namespace: namespace)
 
       visit new_project_path
 
-      select2('2', from: '#project_namespace_id')
+      select2(namespace.id, from: '#project_namespace_id')
 
       # click on disabled element
       find(:link, 'GitLab export').trigger('click')
@@ -81,24 +78,30 @@ feature 'project import', feature: true, js: true do
     end
   end
 
-  context 'normal user' do
+  context 'when limited to the default user namespace' do
+    let(:user) { create(:user) }
     before do
-      login_as(normal_user)
+      login_as(user)
     end
 
-    scenario 'non-admin user is not allowed to import a project' do
-      expect(Project.all.count).to be_zero
-
+    scenario 'passes correct namespace ID in the URL' do
       visit new_project_path
 
       fill_in :project_path, with: 'test-project-path', visible: true
 
-      expect(page).not_to have_content('GitLab export')
+      click_link 'GitLab export'
+
+      expect(page).to have_content('GitLab project export')
+      expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path")
     end
   end
 
-  def wiki_exists?
+  def wiki_exists?(project)
     wiki = ProjectWiki.new(project)
     File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
   end
+
+  def project_hook_exists?(project)
+    Gitlab::Git::Hook.new('post-receive', project.repository.path).exists?
+  end
 end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 7bb0d26b21c03d96db4363bd0543422bcf5df536..bfe59bdb90e75cb869cab6aa86cb9dc81ac5e225 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 4a83740621a698129f07e2e1f8e7c947c0bebe4c..2f377312ea5aa94bc48599b6eaa9093ffc156ec3 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -13,10 +13,13 @@ feature 'issuable templates', feature: true, js: true do
 
   context 'user creates an issue using templates' do
     let(:template_content) { 'this is a test "bug" template' }
+    let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
     let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+    let(:description_addition) { ' appending to description' }
 
     background do
       project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+      project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
       visit edit_namespace_project_issue_path project.namespace, project, issue
       fill_in :'issue[title]', with: 'test issue title'
     end
@@ -27,6 +30,56 @@ feature 'issuable templates', feature: true, js: true do
       preview_template
       save_changes
     end
+
+    scenario 'user selects "bug" template and then "no template"' do
+      select_template 'bug'
+      wait_for_ajax
+      select_option 'No template'
+      wait_for_ajax
+      preview_template('')
+      save_changes('')
+    end
+
+    scenario 'user selects "bug" template, edits description and then selects "reset template"' do
+      select_template 'bug'
+      wait_for_ajax
+      find_field('issue_description').send_keys(description_addition)
+      preview_template(template_content + description_addition)
+      select_option 'Reset template'
+      preview_template
+      save_changes
+    end
+
+    it 'updates height of markdown textarea' do
+      start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+      select_template 'test'
+      wait_for_ajax
+
+      end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+      expect(end_height).not_to eq(start_height)
+    end
+  end
+
+  context 'user creates an issue using templates, with a prior description' do
+    let(:prior_description) { 'test issue description' }
+    let(:template_content) { 'this is a test "bug" template' }
+    let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+
+    background do
+      project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+      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
+    end
+
+    scenario 'user selects "bug" template' do
+      select_template 'bug'
+      wait_for_ajax
+      preview_template("#{template_content}")
+      save_changes
+    end
   end
 
   context 'user creates a merge request using templates' do
@@ -51,7 +104,7 @@ feature 'issuable templates', feature: true, js: true do
     let(:template_content) { 'this is a test "feature-proposal" template' }
     let(:fork_user) { create(:user) }
     let(:fork_project) { create(:project, :public) }
-    let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+    let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) }
 
     background do
       logout
@@ -59,31 +112,41 @@ 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
-      fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
-      visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+      project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+      visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
       fill_in :'merge_request[title]', with: 'test merge request title'
     end
 
-    scenario 'user selects "feature-proposal" template' do
-      select_template 'feature-proposal'
-      wait_for_ajax
-      preview_template
-      save_changes
+    context 'feature proposal template' do
+      context 'template exists in target project' do
+        scenario 'user selects template' do
+          select_template 'feature-proposal'
+          wait_for_ajax
+          preview_template
+          save_changes
+        end
+      end
     end
   end
 
-  def preview_template
+  def preview_template(expected_content = template_content)
     click_link 'Preview'
-    expect(page).to have_content template_content
+    expect(page).to have_content expected_content
+    click_link 'Write'
   end
 
-  def save_changes
+  def save_changes(expected_content = template_content)
     click_button "Save changes"
-    expect(page).to have_content template_content
+    expect(page).to have_content expected_content
   end
 
   def select_template(name)
     first('.js-issuable-selector').click
     first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
   end
+
+  def select_option(name)
+    first('.js-issuable-selector').click
+    first('.js-issuable-selector-wrap .dropdown-footer-list a', text: name).click
+  end
 end
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3137af074cac5e791d83bdaffe10682025a2a54b
--- /dev/null
+++ b/spec/features/projects/issues/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Issues List' do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+
+  background do
+    project.team << [user, :developer]
+
+    login_as(user)
+  end
+
+  scenario 'user does not see create new list button' do
+    create(:issue, project: project)
+
+    visit namespace_project_issues_path(project.namespace, project)
+
+    expect(page).not_to have_selector('.js-new-board-list')
+  end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index cb7495da8ebf4b278dae1dff54547be3ee461753..c9fa8315e79e9d688de3f50f8dda4e3b835ecf48 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -3,18 +3,56 @@ require 'spec_helper'
 feature 'Prioritize labels', feature: true do
   include WaitForAjax
 
-  context 'when project belongs to user' do
-    let(:user)    { create(:user) }
-    let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+  let(:user)     { create(:user) }
+  let(:group)    { create(:group) }
+  let(:project)  { create(:empty_project, :public, namespace: group) }
+  let!(:bug)     { create(:label, project: project, title: 'bug') }
+  let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
+  let!(:feature) { create(:group_label, group: group, title: 'feature') }
 
-    scenario 'user can prioritize a label', js: true do
-      bug     = create(:label, title: 'bug')
-      wontfix = create(:label, title: 'wontfix')
-
-      project.labels << bug
-      project.labels << wontfix
+  context 'when user belongs to project team' do
+    before do
+      project.team << [user, :developer]
 
       login_as user
+    end
+
+    scenario 'user can prioritize a group label', js: true do
+      visit namespace_project_labels_path(project.namespace, project)
+
+      expect(page).to have_content('No prioritized labels yet')
+
+      page.within('.other-labels') do
+        all('.js-toggle-priority')[1].click
+        wait_for_ajax
+        expect(page).not_to have_content('feature')
+      end
+
+      page.within('.prioritized-labels') do
+        expect(page).not_to have_content('No prioritized labels yet')
+        expect(page).to have_content('feature')
+      end
+    end
+
+    scenario 'user can unprioritize a group label', js: true do
+      create(:label_priority, project: project, label: feature, priority: 1)
+
+      visit namespace_project_labels_path(project.namespace, project)
+
+      page.within('.prioritized-labels') do
+        expect(page).to have_content('feature')
+
+        first('.js-toggle-priority').click
+        wait_for_ajax
+        expect(page).not_to have_content('bug')
+      end
+
+      page.within('.other-labels') do
+        expect(page).to have_content('feature')
+      end
+    end
+
+    scenario 'user can prioritize a project label', js: true do
       visit namespace_project_labels_path(project.namespace, project)
 
       expect(page).to have_content('No prioritized labels yet')
@@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do
       end
     end
 
-    scenario 'user can unprioritize a label', js: true do
-      bug     = create(:label, title: 'bug', priority: 1)
-      wontfix = create(:label, title: 'wontfix')
-
-      project.labels << bug
-      project.labels << wontfix
+    scenario 'user can unprioritize a project label', js: true do
+      create(:label_priority, project: project, label: bug, priority: 1)
 
-      login_as user
       visit namespace_project_labels_path(project.namespace, project)
 
-      expect(page).to have_content('bug')
-
       page.within('.prioritized-labels') do
+        expect(page).to have_content('bug')
+
         first('.js-toggle-priority').click
         wait_for_ajax
         expect(page).not_to have_content('bug')
@@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do
     end
 
     scenario 'user can sort prioritized labels and persist across reloads', js: true do
-      bug     = create(:label, title: 'bug', priority: 1)
-      wontfix = create(:label, title: 'wontfix', priority: 2)
-
-      project.labels << bug
-      project.labels << wontfix
+      create(:label_priority, project: project, label: bug, priority: 1)
+      create(:label_priority, project: project, label: feature, priority: 2)
 
-      login_as user
       visit namespace_project_labels_path(project.namespace, project)
 
       expect(page).to have_content 'bug'
+      expect(page).to have_content 'feature'
       expect(page).to have_content 'wontfix'
 
       # Sort labels
-      find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
+      find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
 
       page.within('.prioritized-labels') do
-        expect(first('li')).to have_content('wontfix')
+        expect(first('li')).to have_content('feature')
         expect(page.all('li').last).to have_content('bug')
       end
 
@@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do
       wait_for_ajax
 
       page.within('.prioritized-labels') do
-        expect(first('li')).to have_content('wontfix')
+        expect(first('li')).to have_content('feature')
         expect(page.all('li').last).to have_content('bug')
       end
     end
@@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do
 
   context 'as a guest' do
     it 'does not prioritize labels' do
-      user = create(:user)
       guest = create(:user)
-      project = create(:project, name: 'test', namespace: user.namespace)
-
-      create(:label, title: 'bug')
 
       login_as guest
+
       visit namespace_project_labels_path(project.namespace, project)
 
+      expect(page).to have_content 'bug'
+      expect(page).to have_content 'wontfix'
+      expect(page).to have_content 'feature'
       expect(page).not_to have_css('.prioritized-labels')
     end
   end
 
   context 'as a non signed in user' do
     it 'does not prioritize labels' do
-      user = create(:user)
-      project = create(:project, name: 'test', namespace: user.namespace)
-
-      create(:label, title: 'bug')
-
       visit namespace_project_labels_path(project.namespace, project)
 
+      expect(page).to have_content 'bug'
+      expect(page).to have_content 'wontfix'
+      expect(page).to have_content 'feature'
       expect(page).not_to have_css('.prioritized-labels')
     end
   end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..227ccf9459c3e5728ca47bba445b3bceea13f13c
--- /dev/null
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Download buttons in project main page', feature: true do
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:status) { 'success' }
+  given(:project) { create(:project) }
+
+  given(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit.sha,
+           ref: project.default_branch,
+           status: status)
+  end
+
+  given!(:build) do
+    create(:ci_build, :success, :artifacts,
+           pipeline: pipeline,
+           status: pipeline.status,
+           name: 'build')
+  end
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+  end
+
+  describe 'when checking project main page' do
+    context 'with artifacts' do
+      before do
+        visit namespace_project_path(project.namespace, project)
+      end
+
+      scenario 'shows download artifacts button' do
+        href = latest_succeeded_namespace_project_artifacts_path(
+          project.namespace, project, "#{project.default_branch}/download",
+          job: 'build')
+
+        expect(page).to have_link "Download '#{build.name}'", href: href
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc2f695211ce3e44e220f3351dd0c8fb2f08aeb8
--- /dev/null
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user) { create(:user) }
+  let(:group) { create(:group, :public) }
+  let(:project) { create(:empty_project, :public) }
+
+  background do
+    project.team << [user, :master]
+    @group_link = create(:project_group_link, project: project, group: group)
+
+    login_as(user)
+    visit namespace_project_project_members_path(project.namespace, project)
+  end
+
+  it 'updates group access level' do
+    select 'Guest', from: "member_access_level_#{group.id}"
+    wait_for_ajax
+
+    visit namespace_project_project_members_path(project.namespace, project)
+
+    expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+  end
+
+  it 'updates expiry date' do
+    tomorrow = Date.today + 3
+
+    fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+    wait_for_ajax
+
+    page.within(find('li.group_member')) do
+      expect(page).to have_content('Expires in')
+    end
+  end
+
+  it 'deletes group link' do
+    page.within(first('.group_member')) do
+      find('.btn-remove').click
+    end
+    wait_for_ajax
+
+    expect(page).not_to have_selector('.group_member')
+  end
+
+  context 'search' do
+    it 'finds no results' do
+      page.within '.member-search-form' do
+        fill_in 'search', with: 'testing 123'
+        find('.member-search-btn').click
+      end
+
+      expect(page).not_to have_selector('.group_member')
+    end
+
+    it 'finds results' do
+      page.within '.member-search-form' do
+        fill_in 'search', with: group.name
+        find('.member-search-btn').click
+      end
+
+      expect(page).to have_selector('.group_member', count: 1)
+    end
+  end
+end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27a83fdcd1f6d7235e5b82d52524e2af2ff3379e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+  include WaitForAjax
+  include Select2Helper
+  include ActiveSupport::Testing::TimeHelpers
+
+  let(:master) { create(:user) }
+  let(:project) { create(:project) }
+  let!(:new_member) { create(:user) }
+
+  background do
+    project.team << [master, :master]
+    login_as(master)
+  end
+
+  scenario 'expiration date is displayed in the members list' do
+    travel_to Time.zone.parse('2016-08-06 08:00') do
+      visit namespace_project_project_members_path(project.namespace, project)
+
+      page.within '.users-project-form' do
+        select2(new_member.id, from: '#user_ids', multiple: true)
+        fill_in 'expires_at', with: '2016-08-10'
+        click_on 'Add to project'
+      end
+
+      page.within '.project_member:first-child' do
+        expect(page).to have_content('Expires in 4 days')
+      end
+    end
+  end
+
+  scenario 'change expiration date' do
+    travel_to Time.zone.parse('2016-08-06 08:00') do
+      project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+      visit namespace_project_project_members_path(project.namespace, project)
+
+      page.within '.project_member:first-child' do
+        find('.js-access-expiration-date').set '2016-08-09'
+        wait_for_ajax
+        expect(page).to have_content('Expires in 3 days')
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index f7fcd9b67313be821404ec7c6ff9088e50588dec..d15376931c388d5ecd4cd021a83c367d78919346 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do
 
   def expect_visible_access_request(project, user)
     expect(project.requesters.exists?(user_id: user)).to be_truthy
-    expect(page).to have_content "#{project.name} access requests 1"
+    expect(page).to have_content "Users requesting access to #{project.name} 1"
     expect(page).to have_content user.name
   end
 end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 67811b1048e6c5df768e7e358a23153158447dde..6e948b7a61636643a2a6388c5de1f5d5aba62515 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -1,12 +1,10 @@
 require 'spec_helper'
 
 feature 'Projects > Members > Owner cannot leave project', feature: true do
-  let(:owner) { create(:user) }
   let(:project) { create(:project) }
 
   background do
-    project.team << [owner, :owner]
-    login_as(owner)
+    login_as(project.owner)
     visit namespace_project_path(project.namespace, project)
   end
 
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index 0e54c4fdf20b4c7bdcdf0df8dc6365942a8a9ecc..4ca9272b9c10736f4584ea23c280b79bc898a26a 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -1,12 +1,10 @@
 require 'spec_helper'
 
 feature 'Projects > Members > Owner cannot request access to his project', feature: true do
-  let(:owner) { create(:user) }
   let(:project) { create(:project) }
 
   background do
-    project.team << [owner, :owner]
-    login_as(owner)
+    login_as(project.owner)
     visit namespace_project_path(project.namespace, project)
   end
 
diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5dd58ad66a768ba16d3c34c83b6fa43a3549150b
--- /dev/null
+++ b/spec/features/projects/merge_requests/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Merge Requests List' do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  background do
+    project.team << [user, :developer]
+
+    login_as(user)
+  end
+
+  scenario 'user does not see create new list button' do
+    create(:merge_request, source_project: project)
+
+    visit namespace_project_merge_requests_path(project.namespace, project)
+
+    expect(page).not_to have_selector('.js-new-board-list')
+  end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index 29d150bc5971be934b218e04c53774e7fa6c6a55..db56a50e0584ec8690161b59694c09301c91dcae 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -177,7 +177,7 @@ describe "Pipelines" do
         before { click_on 'Retry failed' }
 
         it { expect(page).not_to have_content('Retry failed') }
-        it { expect(page).to have_content('retried') }
+        it { expect(page).to have_selector('.retried') }
       end
     end
 
@@ -193,7 +193,11 @@ describe "Pipelines" do
     end
 
     context 'playing manual build' do
-      before { click_link('Play') }
+      before do
+        within '.pipeline-holder' do
+          click_link('Play')
+        end
+      end
 
       it { expect(@manual.reload).to be_pending }
     end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index b3ba40b35afc5adc7581fdb924a4434724e74a34..472491188c9aecba02ee75bc021688f8a69fdd34 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -22,8 +22,20 @@ feature 'Ref switcher', feature: true, js: true do
       input.native.send_keys :down
       input.native.send_keys :down
       input.native.send_keys :enter
+    end
+
+    expect(page).to have_title 'expand-collapse-files'
+  end
+
+  it "user selects ref with special characters" do
+    click_button 'master'
+    wait_for_ajax
 
-      expect(page).to have_content 'expand-collapse-files'
+    page.within '.project-refs-form' do
+      page.fill_in 'Search branches and tags', with: "'test'"
+      click_link "'test'"
     end
+
+    expect(page).to have_title "'test'"
   end
 end
diff --git a/spec/features/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
similarity index 87%
rename from spec/features/pipelines_settings_spec.rb
rename to spec/features/projects/settings/pipelines_settings_spec.rb
index dcc364a3d01cd09753777ceda9b9287df29d99ec..76cb240ea98459781b9141542bf05a82ee80afaf 100644
--- a/spec/features/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -24,11 +24,12 @@ feature "Pipelines settings", feature: true do
   context 'for master' do
     given(:role) { :master }
 
-    scenario 'be allowed to change' do
+    scenario 'be allowed to change', js: true do
       fill_in('Test coverage parsing', with: 'coverage_regex')
       click_on 'Save changes'
 
       expect(page.status_code).to eq(200)
+      expect(page).to have_button('Save changes', disabled: false)
       expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
     end
   end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d37e8ed4699e099a0d13d12ae9080d10cc640759
--- /dev/null
+++ b/spec/features/projects/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Project snippets', feature: true do
+  context 'when the project has snippets' do
+    let(:project) { create(:empty_project, :public) }
+    let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+    before do
+      allow(Snippet).to receive(:default_per_page).and_return(1)
+      visit namespace_project_snippets_path(project.namespace, project)
+    end
+
+    it_behaves_like 'paginated snippets'
+  end
+end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dd93d25c2c6b8e15e57749a2767447a8f8c76579
--- /dev/null
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Download buttons in tags page', feature: true do
+  given(:user) { create(:user) }
+  given(:role) { :developer }
+  given(:status) { 'success' }
+  given(:tag) { 'v1.0.0' }
+  given(:project) { create(:project) }
+
+  given(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit(tag).sha,
+           ref: tag,
+           status: status)
+  end
+
+  given!(:build) do
+    create(:ci_build, :success, :artifacts,
+           pipeline: pipeline,
+           status: pipeline.status,
+           name: 'build')
+  end
+
+  background do
+    login_as(user)
+    project.team << [user, role]
+  end
+
+  describe 'when checking tags' do
+    context 'with artifacts' do
+      before do
+        visit namespace_project_tags_path(project.namespace, project)
+      end
+
+      scenario 'shows download artifacts button' do
+        href = latest_succeeded_namespace_project_artifacts_path(
+          project.namespace, project, "#{tag}/download",
+          job: 'build')
+
+        expect(page).to have_link "Download '#{build.name}'", href: href
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b4f5f6b3fc5f10ede29a8e8f354dc8a157cf04e2
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views wiki in project page', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+  end
+
+  context 'when repository is disabled for project' do
+    before do
+      project.project_feature.update!(
+        repository_access_level: ProjectFeature::DISABLED,
+        merge_requests_access_level: ProjectFeature::DISABLED,
+        builds_access_level: ProjectFeature::DISABLED
+      )
+    end
+
+    context 'when wiki homepage contains a link' do
+      before do
+        WikiPages::CreateService.new(
+          project,
+          user,
+          title: 'home',
+          content: '[some link](other-page)'
+        ).execute
+      end
+
+      it 'displays the correct URL for the link' do
+        visit namespace_project_path(project.namespace, project)
+        expect(page).to have_link(
+          'some link',
+          href: namespace_project_wiki_path(
+            project.namespace,
+            project,
+            'other-page'
+          )
+        )
+      end
+    end
+  end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 1b14c66fe286ec30fa059e7773f2d6c34667235e..c30d38b650891b5cbfbcf5f448d1f9cd2cb770fe 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -57,7 +57,7 @@ feature 'Project', feature: true do
 
   describe 'removal', js: true do
     let(:user)    { create(:user) }
-    let(:project) { create(:project, namespace: user.namespace) }
+    let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
 
     before do
       login_with(user)
@@ -65,8 +65,12 @@ feature 'Project', feature: true do
       visit edit_namespace_project_path(project.namespace, project)
     end
 
-    it 'removes project' 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(Project.all.count).to be_zero
+      expect(project.issues).to be_empty
+      expect(project.merge_requests).to be_empty
     end
   end
 
@@ -78,7 +82,7 @@ feature 'Project', feature: true do
 
     before do
       login_with(user)
-      project.team.add_user(user, Gitlab::Access::MASTER)
+      project.add_user(user, Gitlab::Access::MASTER)
       visit namespace_project_path(project.namespace, project)
     end
 
@@ -97,8 +101,8 @@ feature 'Project', feature: true do
     context 'on issues page', js: true do
       before do
         login_with(user)
-        project.team.add_user(user, Gitlab::Access::MASTER)
-        project2.team.add_user(user, Gitlab::Access::MASTER)
+        project.add_user(user, Gitlab::Access::MASTER)
+        project2.add_user(user, Gitlab::Access::MASTER)
         visit namespace_project_issue_path(project.namespace, project, issue)
       end
 
@@ -115,6 +119,35 @@ feature 'Project', feature: true do
     end
   end
 
+  describe 'tree view (default view is set to Files)' do
+    let(:user) { create(:user, project_view: 'files') }
+    let(:project) { create(:forked_project_with_submodules) }
+
+    before do
+      project.team << [user, :master]
+      login_as user
+      visit namespace_project_path(project.namespace, project)
+    end
+
+    it 'has working links to files' do
+      click_link('PROCESS.md')
+
+      expect(page.status_code).to eq(200)
+    end
+
+    it 'has working links to directories' do
+      click_link('encoding')
+
+      expect(page.status_code).to eq(200)
+    end
+
+    it 'has working links to submodules' do
+      click_link('645f6c4c')
+
+      expect(page.status_code).to eq(200)
+    end
+  end
+
   def remove_with_confirm(button_text, confirm_with)
     click_button button_text
     fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..395c61a47438e9b33e0f659445d0449a6963650f
--- /dev/null
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -0,0 +1,71 @@
+RSpec.shared_examples "protected branches > access control > CE" do
+  ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+    it "allows creating protected branches that #{access_type_name} can push to" do
+      visit namespace_project_protected_branches_path(project.namespace, project)
+      set_protected_branch_name('master')
+      within('.new_protected_branch') do
+        allowed_to_push_button = find(".js-allowed-to-push")
+
+        unless allowed_to_push_button.text == access_type_name
+          allowed_to_push_button.click
+          within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+        end
+      end
+      click_on "Protect"
+
+      expect(ProtectedBranch.count).to eq(1)
+      expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
+    end
+
+    it "allows updating protected branches so that #{access_type_name} can push to them" do
+      visit namespace_project_protected_branches_path(project.namespace, project)
+      set_protected_branch_name('master')
+      click_on "Protect"
+
+      expect(ProtectedBranch.count).to eq(1)
+
+      within(".protected-branches-list") do
+        find(".js-allowed-to-push").click
+        within('.js-allowed-to-push-container') { click_on access_type_name }
+      end
+
+      wait_for_ajax
+      expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+    end
+  end
+
+  ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+    it "allows creating protected branches that #{access_type_name} can merge to" do
+      visit namespace_project_protected_branches_path(project.namespace, project)
+      set_protected_branch_name('master')
+      within('.new_protected_branch') do
+        allowed_to_merge_button = find(".js-allowed-to-merge")
+
+        unless allowed_to_merge_button.text == access_type_name
+          allowed_to_merge_button.click
+          within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+        end
+      end
+      click_on "Protect"
+
+      expect(ProtectedBranch.count).to eq(1)
+      expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+    end
+
+    it "allows updating protected branches so that #{access_type_name} can merge to them" do
+      visit namespace_project_protected_branches_path(project.namespace, project)
+      set_protected_branch_name('master')
+      click_on "Protect"
+
+      expect(ProtectedBranch.count).to eq(1)
+
+      within(".protected-branches-list") do
+        find(".js-allowed-to-merge").click
+        within('.js-allowed-to-merge-container') { click_on access_type_name }
+      end
+
+      wait_for_ajax
+      expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
+    end
+  end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index a0ee6cab7ec9b429d5aad02ae7e8666ae832c059..1a3f7b970f6c81881e73bd833693e97937fe990e 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,4 +1,5 @@
 require 'spec_helper'
+Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
 
 feature 'Projected Branches', feature: true, js: true do
   include WaitForAjax
@@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do
   end
 
   describe "access control" do
-    ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
-      it "allows creating protected branches that #{access_type_name} can push to" do
-        visit namespace_project_protected_branches_path(project.namespace, project)
-        set_protected_branch_name('master')
-        within('.new_protected_branch') do
-          allowed_to_push_button = find(".js-allowed-to-push")
-
-          unless allowed_to_push_button.text == access_type_name
-            allowed_to_push_button.click
-            within(".dropdown.open .dropdown-menu") { click_on access_type_name }
-          end
-        end
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-        expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
-      end
-
-      it "allows updating protected branches so that #{access_type_name} can push to them" do
-        visit namespace_project_protected_branches_path(project.namespace, project)
-        set_protected_branch_name('master')
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-
-        within(".protected-branches-list") do
-          find(".js-allowed-to-push").click
-          within('.js-allowed-to-push-container') { click_on access_type_name }
-        end
-
-        wait_for_ajax
-        expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
-      end
-    end
-
-    ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
-      it "allows creating protected branches that #{access_type_name} can merge to" do
-        visit namespace_project_protected_branches_path(project.namespace, project)
-        set_protected_branch_name('master')
-        within('.new_protected_branch') do
-          allowed_to_merge_button = find(".js-allowed-to-merge")
-
-          unless allowed_to_merge_button.text == access_type_name
-            allowed_to_merge_button.click
-            within(".dropdown.open .dropdown-menu") { click_on access_type_name }
-          end
-        end
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-        expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
-      end
-
-      it "allows updating protected branches so that #{access_type_name} can merge to them" do
-        visit namespace_project_protected_branches_path(project.namespace, project)
-        set_protected_branch_name('master')
-        click_on "Protect"
-
-        expect(ProtectedBranch.count).to eq(1)
-
-        within(".protected-branches-list") do
-          find(".js-allowed-to-merge").click
-          within('.js-allowed-to-merge-container') { click_on access_type_name }
-        end
-
-        wait_for_ajax
-        expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
-      end
-    end
+    include_examples "protected branches > access control > CE"
   end
 end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index a5ed3595b0a441b341f02baf2f06bdb1228cb1e9..0e1cc9a0f73ad72be4be90b0e377457050eeeaba 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -60,7 +60,7 @@ describe "Runners" do
 
     it "removes specific runner for project if this is last project for that runners" do
       within ".activated-specific-runners" do
-        click_on "Remove runner"
+        click_on "Remove Runner"
       end
 
       expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
@@ -75,7 +75,7 @@ describe "Runners" do
     end
 
     it "enables shared runners" do
-      click_on "Enable shared runners"
+      click_on "Enable shared Runners"
       expect(@project.reload.shared_runners_enabled).to be_truthy
     end
   end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index b7a25d80fec90a01565e7fd328a58d45706fa530..caecd027aaa4bda68c48387b3f3a5aa1e437e295 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe "Search", feature: true  do
+  include WaitForAjax
+
   let(:user) { create(:user) }
   let(:project) { create(:project, namespace: user.namespace) }
   let!(:issue) { create(:issue, project: project, assignee: user) }
@@ -16,6 +18,36 @@ describe "Search", feature: true  do
     expect(page).not_to have_selector('.search')
   end
 
+  context 'search filters', js: true do
+    let(:group) { create(:group) }
+
+    before do
+      group.add_owner(user)
+    end
+
+    it 'shows group name after filtering' do
+      find('.js-search-group-dropdown').click
+      wait_for_ajax
+
+      page.within '.search-holder' do
+        click_link group.name
+      end
+
+      expect(find('.js-search-group-dropdown')).to have_content(group.name)
+    end
+
+    it 'shows project name after filtering' do
+      page.within('.project-filter') do
+        find('.js-search-project-dropdown').click
+        wait_for_ajax
+
+        click_link project.name_with_namespace
+      end
+
+      expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+    end
+  end
+
   describe 'searching for Projects' do
     it 'finds a project' do
       page.within '.search-holder' do
@@ -68,9 +100,45 @@ describe "Search", feature: true  do
 
       expect(page).to have_link(snippet.title)
     end
+
+    it 'finds a commit' do
+      visit namespace_project_path(project.namespace, project)
+
+      page.within '.search' do
+        fill_in 'search', with: 'add'
+        click_button 'Go'
+      end
+
+      click_link "Commits"
+
+      expect(page).to have_selector('.commit-row-description')
+    end
+
+    it 'finds a code' do
+      visit namespace_project_path(project.namespace, project)
+
+      page.within '.search' do
+        fill_in 'search', with: 'def'
+        click_button 'Go'
+      end
+
+      click_link "Code"
+
+      expect(page).to have_selector('.file-content .code')
+    end
   end
 
   describe 'Right header search field', feature: true do
+    it 'allows enter key to search', js: true do
+      visit namespace_project_path(project.namespace, project)
+      fill_in 'search', with: 'gitlab'
+      find('#search').native.send_keys(:enter)
+
+      page.within '.title' do
+        expect(page).to have_content 'Search'
+      end
+    end
+
     describe 'Search in project page' do
       before do
         visit namespace_project_path(project.namespace, project)
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb924925e3111d1ec422fdfeb263970..40f773956d198606e35e917ff0eae75295757225 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true  do
     it { is_expected.to be_allowed_for :visitor }
   end
 
+  describe "GET /koding" do
+    subject { koding_path }
+
+    context 'with Koding enabled' do
+      before do
+        stub_application_setting(koding_enabled?: true)
+      end
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_denied_for :visitor }
+    end
+  end
+
   describe "GET /projects/new" do
     it { expect(new_project_path).to be_allowed_for :admin }
     it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ccb5c06dab013144d0d6974d892a9ecb40357954..79417c769a87069f6bbbf163cddd0aead9458baf 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -203,7 +203,7 @@ describe "Private Project Access", feature: true  do
     it { is_expected.to be_allowed_for master }
     it { is_expected.to be_allowed_for developer }
     it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
+    it { is_expected.to be_denied_for  guest }
     it { is_expected.to be_denied_for :user }
     it { is_expected.to be_denied_for :external }
     it { is_expected.to be_denied_for :visitor }
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index db53a9cec9747c7191a21f6ba516a629f5ed7459..49deacc5c7437180c0fae8a73315b0c038010b01 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe "Internal Project Snippets Access", feature: true  do
   include AccessMatchers
 
-  let(:project) { create(:project, :internal) }
+  let(:project) { create(:empty_project, :internal) }
 
   let(:owner)     { project.owner }
   let(:master)    { create(:user) }
@@ -48,31 +48,63 @@ describe "Internal Project Snippets Access", feature: true  do
     it { is_expected.to be_denied_for :visitor }
   end
 
-  describe "GET /:project_path/snippets/:id for an internal snippet" do
-    subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
+  describe "GET /:project_path/snippets/:id" do
+    context "for an internal snippet" do
+      subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
 
-    it { is_expected.to be_allowed_for :admin }
-    it { is_expected.to be_allowed_for owner }
-    it { is_expected.to be_allowed_for master }
-    it { is_expected.to be_allowed_for developer }
-    it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
-    it { is_expected.to be_allowed_for :user }
-    it { is_expected.to be_denied_for :external }
-    it { is_expected.to be_denied_for :visitor }
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_denied_for :external }
+      it { is_expected.to be_denied_for :visitor }
+    end
+
+    context "for a private snippet" do
+      subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      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
   end
 
-  describe "GET /:project_path/snippets/:id for a private snippet" do
-    subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+  describe "GET /:project_path/snippets/:id/raw" do
+    context "for an internal snippet" do
+      subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) }
 
-    it { is_expected.to be_allowed_for :admin }
-    it { is_expected.to be_allowed_for owner }
-    it { is_expected.to be_allowed_for master }
-    it { is_expected.to be_allowed_for developer }
-    it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
-    it { is_expected.to be_denied_for :user }
-    it { is_expected.to be_denied_for :external }
-    it { is_expected.to be_denied_for :visitor }
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_denied_for :external }
+      it { is_expected.to be_denied_for :visitor }
+    end
+
+    context "for a private snippet" do
+      subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      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
   end
 end
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index d23d645c8e56f8a05b3ae0e9fd52f32072c39cee..a1bfc076d99527533163a61fd5d0811ff981a3a6 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe "Private Project Snippets Access", feature: true  do
   include AccessMatchers
 
-  let(:project) { create(:project, :private) }
+  let(:project) { create(:empty_project, :private) }
 
   let(:owner)     { project.owner }
   let(:master)    { create(:user) }
@@ -60,4 +60,18 @@ describe "Private Project Snippets Access", feature: true  do
     it { is_expected.to be_denied_for :external }
     it { is_expected.to be_denied_for :visitor }
   end
+
+  describe "GET /:project_path/snippets/:id/raw for a private snippet" do
+    subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+    it { is_expected.to be_allowed_for :admin }
+    it { is_expected.to be_allowed_for owner }
+    it { is_expected.to be_allowed_for master }
+    it { is_expected.to be_allowed_for developer }
+    it { is_expected.to be_allowed_for reporter }
+    it { is_expected.to be_allowed_for guest }
+    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
 end
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index e3665b6116ab83e0953cc0c9f5d3f9a70b7421f6..30bcd87ef049622a8a23a073a2b796323fb20448 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe "Public Project Snippets Access", feature: true  do
   include AccessMatchers
 
-  let(:project) { create(:project, :public) }
+  let(:project) { create(:empty_project, :public) }
 
   let(:owner)     { project.owner }
   let(:master)    { create(:user) }
@@ -49,45 +49,91 @@ describe "Public Project Snippets Access", feature: true  do
     it { is_expected.to be_denied_for :visitor }
   end
 
-  describe "GET /:project_path/snippets/:id for a public snippet" do
-    subject { namespace_project_snippet_path(project.namespace, project, public_snippet) }
+  describe "GET /:project_path/snippets/:id" do
+    context "for a public snippet" do
+      subject { namespace_project_snippet_path(project.namespace, project, public_snippet) }
 
-    it { is_expected.to be_allowed_for :admin }
-    it { is_expected.to be_allowed_for owner }
-    it { is_expected.to be_allowed_for master }
-    it { is_expected.to be_allowed_for developer }
-    it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
-    it { is_expected.to be_allowed_for :user }
-    it { is_expected.to be_allowed_for :external }
-    it { is_expected.to be_allowed_for :visitor }
-  end
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_allowed_for :external }
+      it { is_expected.to be_allowed_for :visitor }
+    end
 
-  describe "GET /:project_path/snippets/:id for an internal snippet" do
-    subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
+    context "for an internal snippet" do
+      subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
 
-    it { is_expected.to be_allowed_for :admin }
-    it { is_expected.to be_allowed_for owner }
-    it { is_expected.to be_allowed_for master }
-    it { is_expected.to be_allowed_for developer }
-    it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
-    it { is_expected.to be_allowed_for :user }
-    it { is_expected.to be_denied_for :external }
-    it { is_expected.to be_denied_for :visitor }
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_denied_for :external }
+      it { is_expected.to be_denied_for :visitor }
+    end
+
+    context "for a private snippet" do
+      subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      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
   end
 
-  describe "GET /:project_path/snippets/:id for a private snippet" do
-    subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
+  describe "GET /:project_path/snippets/:id/raw" do
+    context "for a public snippet" do
+      subject { raw_namespace_project_snippet_path(project.namespace, project, public_snippet) }
 
-    it { is_expected.to be_allowed_for :admin }
-    it { is_expected.to be_allowed_for owner }
-    it { is_expected.to be_allowed_for master }
-    it { is_expected.to be_allowed_for developer }
-    it { is_expected.to be_allowed_for reporter }
-    it { is_expected.to be_allowed_for guest }
-    it { is_expected.to be_denied_for :user }
-    it { is_expected.to be_denied_for :external }
-    it { is_expected.to be_denied_for :visitor }
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_allowed_for :external }
+      it { is_expected.to be_allowed_for :visitor }
+    end
+
+    context "for an internal snippet" do
+      subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) }
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      it { is_expected.to be_allowed_for :user }
+      it { is_expected.to be_denied_for :external }
+      it { is_expected.to be_denied_for :visitor }
+    end
+
+    context "for a private snippet" do
+      subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
+
+      it { is_expected.to be_allowed_for :admin }
+      it { is_expected.to be_allowed_for owner }
+      it { is_expected.to be_allowed_for master }
+      it { is_expected.to be_allowed_for developer }
+      it { is_expected.to be_allowed_for reporter }
+      it { is_expected.to be_allowed_for guest }
+      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
   end
 end
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index a752c1d7235aff87995af8a82a9889bfe3cf9d59..65544f79eba817d192cbe90d3ee8b1caa89d8e6b 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -14,7 +14,7 @@ feature 'Signup', feature: true do
         fill_in 'new_user_username', with: user.username
         fill_in 'new_user_email',    with: user.email
         fill_in 'new_user_password', with: user.password
-        click_button "Sign up"
+        click_button "Register"
 
         expect(current_path).to eq users_almost_there_path
         expect(page).to have_content("Please check your email to confirm your account")
@@ -33,7 +33,7 @@ feature 'Signup', feature: true do
         fill_in 'new_user_username', with: user.username
         fill_in 'new_user_email',    with: user.email
         fill_in 'new_user_password', with: user.password
-        click_button "Sign up"
+        click_button "Register"
 
         expect(current_path).to eq dashboard_projects_path
         expect(page).to have_content("Welcome! You have signed up successfully.")
@@ -52,7 +52,7 @@ feature 'Signup', feature: true do
       fill_in 'new_user_username', with: user.username
       fill_in 'new_user_email',    with: existing_user.email
       fill_in 'new_user_password', with: user.password
-      click_button "Sign up"
+      click_button "Register"
 
       expect(current_path).to eq user_registration_path
       expect(page).to have_content("error prohibited this user from being saved")
@@ -69,7 +69,7 @@ feature 'Signup', feature: true do
       fill_in 'new_user_username', with: user.username
       fill_in 'new_user_email',    with: existing_user.email
       fill_in 'new_user_password', with: user.password
-      click_button "Sign up"
+      click_button "Register"
 
       expect(current_path).to eq user_registration_path
       expect(page.body).not_to match(/#{user.password}/)
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34300ccb9407ca1183794567dc2130aeb2896b45
--- /dev/null
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+feature 'Public Snippets', feature: true do
+  scenario 'Unauthenticated user should see public snippets' do
+    public_snippet = create(:personal_snippet, :public)
+
+    visit snippet_path(public_snippet)
+
+    expect(page).to have_content(public_snippet.content)
+  end
+
+  scenario 'Unauthenticated user should see raw public snippets' do
+    public_snippet = create(:personal_snippet, :public)
+
+    visit raw_snippet_path(public_snippet)
+
+    expect(page).to have_content(public_snippet.content)
+  end
+end
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..70b16bfc810e55b51905744a9d1a5b034b9cdb10
--- /dev/null
+++ b/spec/features/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Snippets', feature: true do
+  context 'when the project has snippets' do
+    let(:project) { create(:empty_project, :public) }
+    let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+    before do
+      allow(Snippet).to receive(:default_per_page).and_return(1)
+      visit snippets_path(username: project.owner.username)
+    end
+
+    it_behaves_like 'paginated snippets'
+  end
+end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 6ed279ef9be6f3413b683b6c793e72cf7ca6e2ad..abb27c90e0a969ec83590d5162079c886ad663e5 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do
     MARKDOWN
   end
 
+  let(:singleIncompleteMarkdown) do
+    <<-MARKDOWN.strip_heredoc
+    This is a task list:
+
+    - [ ] Incomplete entry 1
+    MARKDOWN
+  end
+
+  let(:singleCompleteMarkdown) do
+    <<-MARKDOWN.strip_heredoc
+    This is a task list:
+
+    - [x] Incomplete entry 1
+    MARKDOWN
+  end
+
   before do
     Warden.test_mode!
 
@@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do
   end
 
   describe 'for Issues' do
-    let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
+    describe 'multiple tasks' do
+      let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
 
-    it 'renders' do
-      visit_issue(project, issue)
+      it 'renders' do
+        visit_issue(project, issue)
 
-      expect(page).to have_selector('ul.task-list',      count: 1)
-      expect(page).to have_selector('li.task-list-item', count: 6)
-      expect(page).to have_selector('ul input[checked]', count: 2)
-    end
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 6)
+        expect(page).to have_selector('ul input[checked]', count: 2)
+      end
+
+      it 'contains the required selectors' do
+        visit_issue(project, issue)
+
+        container = '.detail-page-description .description.js-task-list-container'
 
-    it 'contains the required selectors' do
-      visit_issue(project, issue)
+        expect(page).to have_selector(container)
+        expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+        expect(page).to have_selector("#{container} .js-task-list-field")
+        expect(page).to have_selector('form.js-issuable-update')
+        expect(page).to have_selector('a.btn-close')
+      end
 
-      container = '.detail-page-description .description.js-task-list-container'
+      it 'is only editable by author' do
+        visit_issue(project, issue)
+        expect(page).to have_selector('.js-task-list-container')
 
-      expect(page).to have_selector(container)
-      expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
-      expect(page).to have_selector("#{container} .js-task-list-field")
-      expect(page).to have_selector('form.js-issuable-update')
-      expect(page).to have_selector('a.btn-close')
+        logout(:user)
+
+        login_as(user2)
+        visit current_path
+        expect(page).not_to have_selector('.js-task-list-container')
+      end
+
+      it 'provides a summary on Issues#index' do
+        visit namespace_project_issues_path(project.namespace, project)
+        expect(page).to have_content("2 of 6 tasks completed")
+      end
     end
 
-    it 'is only editable by author' do
-      visit_issue(project, issue)
-      expect(page).to have_selector('.js-task-list-container')
+    describe 'single incomplete task' do
+      let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
 
-      logout(:user)
+      it 'renders' do
+        visit_issue(project, issue)
 
-      login_as(user2)
-      visit current_path
-      expect(page).not_to have_selector('.js-task-list-container')
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 1)
+        expect(page).to have_selector('ul input[checked]', count: 0)
+      end
+
+      it 'provides a summary on Issues#index' do
+        visit namespace_project_issues_path(project.namespace, project)
+        expect(page).to have_content("0 of 1 task completed")
+      end
     end
 
-    it 'provides a summary on Issues#index' do
-      visit namespace_project_issues_path(project.namespace, project)
-      expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+    describe 'single complete task' do
+      let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
+
+      it 'renders' do
+        visit_issue(project, issue)
+
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 1)
+        expect(page).to have_selector('ul input[checked]', count: 1)
+      end
+
+      it 'provides a summary on Issues#index' do
+        visit namespace_project_issues_path(project.namespace, project)
+        expect(page).to have_content("1 of 1 task completed")
+      end
     end
   end
 
   describe 'for Notes' do
     let!(:issue) { create(:issue, author: user, project: project) }
-    let!(:note) do
-      create(:note, note: markdown, noteable: issue,
-                    project: project, author: user)
+    describe 'multiple tasks' do
+      let!(:note) do
+        create(:note, note: markdown, noteable: issue,
+                      project: project, author: user)
+      end
+
+      it 'renders for note body' do
+        visit_issue(project, issue)
+
+        expect(page).to have_selector('.note ul.task-list',      count: 1)
+        expect(page).to have_selector('.note li.task-list-item', count: 6)
+        expect(page).to have_selector('.note ul input[checked]', count: 2)
+      end
+
+      it 'contains the required selectors' do
+        visit_issue(project, issue)
+
+        expect(page).to have_selector('.note .js-task-list-container')
+        expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
+        expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+      end
+
+      it 'is only editable by author' do
+        visit_issue(project, issue)
+        expect(page).to have_selector('.js-task-list-container')
+
+        logout(:user)
+
+        login_as(user2)
+        visit current_path
+        expect(page).not_to have_selector('.js-task-list-container')
+      end
     end
 
-    it 'renders for note body' do
-      visit_issue(project, issue)
-
-      expect(page).to have_selector('.note ul.task-list',      count: 1)
-      expect(page).to have_selector('.note li.task-list-item', count: 6)
-      expect(page).to have_selector('.note ul input[checked]', count: 2)
-    end
+    describe 'single incomplete task' do
+      let!(:note) do
+        create(:note, note: singleIncompleteMarkdown, noteable: issue,
+                      project: project, author: user)
+      end
 
-    it 'contains the required selectors' do
-      visit_issue(project, issue)
+      it 'renders for note body' do
+        visit_issue(project, issue)
 
-      expect(page).to have_selector('.note .js-task-list-container')
-      expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
-      expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+        expect(page).to have_selector('.note ul.task-list',      count: 1)
+        expect(page).to have_selector('.note li.task-list-item', count: 1)
+        expect(page).to have_selector('.note ul input[checked]', count: 0)
+      end
     end
 
-    it 'is only editable by author' do
-      visit_issue(project, issue)
-      expect(page).to have_selector('.js-task-list-container')
+    describe 'single complete task' do
+      let!(:note) do
+        create(:note, note: singleCompleteMarkdown, noteable: issue,
+                      project: project, author: user)
+      end
 
-      logout(:user)
+      it 'renders for note body' do
+        visit_issue(project, issue)
 
-      login_as(user2)
-      visit current_path
-      expect(page).not_to have_selector('.js-task-list-container')
+        expect(page).to have_selector('.note ul.task-list',      count: 1)
+        expect(page).to have_selector('.note li.task-list-item', count: 1)
+        expect(page).to have_selector('.note ul input[checked]', count: 1)
+      end
     end
   end
 
@@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do
       visit namespace_project_merge_request_path(project.namespace, project, merge)
     end
 
-    let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
+    describe 'multiple tasks' do
+      let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
 
-    it 'renders for description' do
-      visit_merge_request(project, merge)
+      it 'renders for description' do
+        visit_merge_request(project, merge)
 
-      expect(page).to have_selector('ul.task-list',      count: 1)
-      expect(page).to have_selector('li.task-list-item', count: 6)
-      expect(page).to have_selector('ul input[checked]', count: 2)
-    end
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 6)
+        expect(page).to have_selector('ul input[checked]', count: 2)
+      end
 
-    it 'contains the required selectors' do
-      visit_merge_request(project, merge)
+      it 'contains the required selectors' do
+        visit_merge_request(project, merge)
 
-      container = '.detail-page-description .description.js-task-list-container'
+        container = '.detail-page-description .description.js-task-list-container'
 
-      expect(page).to have_selector(container)
-      expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
-      expect(page).to have_selector("#{container} .js-task-list-field")
-      expect(page).to have_selector('form.js-issuable-update')
-      expect(page).to have_selector('a.btn-close')
-    end
+        expect(page).to have_selector(container)
+        expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+        expect(page).to have_selector("#{container} .js-task-list-field")
+        expect(page).to have_selector('form.js-issuable-update')
+        expect(page).to have_selector('a.btn-close')
+      end
 
-    it 'is only editable by author' do
-      visit_merge_request(project, merge)
-      expect(page).to have_selector('.js-task-list-container')
+      it 'is only editable by author' do
+        visit_merge_request(project, merge)
+        expect(page).to have_selector('.js-task-list-container')
 
-      logout(:user)
+        logout(:user)
 
-      login_as(user2)
-      visit current_path
-      expect(page).not_to have_selector('.js-task-list-container')
+        login_as(user2)
+        visit current_path
+        expect(page).not_to have_selector('.js-task-list-container')
+      end
+
+      it 'provides a summary on MergeRequests#index' do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+        expect(page).to have_content("2 of 6 tasks completed")
+      end
+    end
+    
+    describe 'single incomplete task' do
+      let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
+
+      it 'renders for description' do
+        visit_merge_request(project, merge)
+
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 1)
+        expect(page).to have_selector('ul input[checked]', count: 0)
+      end
+
+      it 'provides a summary on MergeRequests#index' do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+        expect(page).to have_content("0 of 1 task completed")
+      end
     end
 
-    it 'provides a summary on MergeRequests#index' do
-      visit namespace_project_merge_requests_path(project.namespace, project)
-      expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+    describe 'single complete task' do
+      let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) }
+
+      it 'renders for description' do
+        visit_merge_request(project, merge)
+
+        expect(page).to have_selector('ul.task-list',      count: 1)
+        expect(page).to have_selector('li.task-list-item', count: 1)
+        expect(page).to have_selector('ul input[checked]', count: 1)
+      end
+
+      it 'provides a summary on MergeRequests#index' do
+        visit namespace_project_merge_requests_path(project.namespace, project)
+        expect(page).to have_content("1 of 1 task completed")
+      end
     end
   end
 end
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d1f2bc788849f6fa77b386a9613268a1f52962d9
--- /dev/null
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe 'Dashboard > User filters todos', feature: true, js: true do
+  include WaitForAjax
+
+  let(:user_1)    { create(:user, username: 'user_1', name: 'user_1') }
+  let(:user_2)    { create(:user, username: 'user_2', name: 'user_2') }
+
+  let(:project_1) { create(:empty_project, name: 'project_1') }
+  let(:project_2) { create(:empty_project, name: 'project_2') }
+
+  let(:issue) { create(:issue, title: 'issue', project: project_1) }
+
+  let!(:merge_request) { create(:merge_request, source_project: project_2, title: 'merge_request') }
+
+  before do
+    create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1)
+    create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2)
+
+    project_1.team << [user_1, :developer]
+    project_2.team << [user_1, :developer]
+    login_as(user_1)
+    visit dashboard_todos_path
+  end
+
+  it 'filters by project' do
+    click_button 'Project'
+    within '.dropdown-menu-project' do
+      fill_in 'Search projects', with: project_1.name_with_namespace
+      click_link project_1.name_with_namespace
+    end
+
+    wait_for_ajax
+
+    expect(page).to     have_content project_1.name_with_namespace
+    expect(page).not_to have_content project_2.name_with_namespace
+  end
+
+  context "Author filter" do
+    it 'filters by author' do
+      click_button 'Author'
+
+      within '.dropdown-menu-author' do
+        fill_in 'Search authors', with: user_1.name
+        click_link user_1.name
+      end
+
+      wait_for_ajax
+
+      expect(find('.todos-list')).to     have_content user_1.name
+      expect(find('.todos-list')).not_to have_content user_2.name
+    end
+
+    it "shows only authors of existing todos" do
+      click_button 'Author'
+
+      within '.dropdown-menu-author' do
+        # It should contain two users + "Any Author"
+        expect(page).to have_selector('.dropdown-menu-user-link', count: 3)
+        expect(page).to have_content(user_1.name)
+        expect(page).to have_content(user_2.name)
+      end
+    end
+
+    it "shows only authors of existing done todos" do
+      user_3 = create :user
+      user_4 = create :user
+      create(:todo, user: user_1, author: user_3, project: project_1, target: issue, action: 1, state: :done)
+      create(:todo, user: user_1, author: user_4, project: project_2, target: merge_request, action: 2, state: :done)
+
+      project_1.team << [user_3, :developer]
+      project_2.team << [user_4, :developer]
+
+      visit dashboard_todos_path(state: 'done')
+
+      click_button 'Author'
+
+      within '.dropdown-menu-author' do
+        # It should contain two users + "Any Author"
+        expect(page).to have_selector('.dropdown-menu-user-link', count: 3)
+        expect(page).to have_content(user_3.name)
+        expect(page).to have_content(user_4.name)
+        expect(page).not_to have_content(user_1.name)
+        expect(page).not_to have_content(user_2.name)
+      end
+    end
+  end
+
+  it 'filters by type' do
+    click_button 'Type'
+    within '.dropdown-menu-type' do
+      click_link 'Issue'
+    end
+
+    wait_for_ajax
+
+    expect(find('.todos-list')).to     have_content issue.to_reference
+    expect(find('.todos-list')).not_to have_content merge_request.to_reference
+  end
+
+  it 'filters by action' do
+    click_button 'Action'
+    within '.dropdown-menu-action' do
+      click_link 'Assigned'
+    end
+
+    wait_for_ajax
+
+    expect(find('.todos-list')).to     have_content ' assigned you '
+    expect(find('.todos-list')).not_to have_content ' mentioned '
+  end
+end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fec28c55d30e80fad4e52867f79097c6429827bb
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+  let(:user)    { create(:user) }
+  let(:project) { create(:empty_project) }
+
+  let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+  let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+  let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+
+  before { project.team << [user, :developer] }
+
+  context 'sort options' do
+    let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+    let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+    let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+    let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+    let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+    before do
+      create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+      create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+      create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+      create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+      create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+      merge_request_1.labels << label_1
+      issue_3.labels         << label_1
+      issue_2.labels         << label_3
+      issue_1.labels         << label_2
+
+      login_as(user)
+      visit dashboard_todos_path
+    end
+
+    it "sorts with oldest created todos first" do
+      click_link "Last created"
+
+      results_list = page.find('.todos-list')
+      expect(results_list.all('p')[0]).to have_content("merge_request_1")
+      expect(results_list.all('p')[1]).to have_content("issue_1")
+      expect(results_list.all('p')[2]).to have_content("issue_3")
+      expect(results_list.all('p')[3]).to have_content("issue_2")
+      expect(results_list.all('p')[4]).to have_content("issue_4")
+    end
+
+    it "sorts with newest created todos first" do
+      click_link "Oldest created"
+
+      results_list = page.find('.todos-list')
+      expect(results_list.all('p')[0]).to have_content("issue_4")
+      expect(results_list.all('p')[1]).to have_content("issue_2")
+      expect(results_list.all('p')[2]).to have_content("issue_3")
+      expect(results_list.all('p')[3]).to have_content("issue_1")
+      expect(results_list.all('p')[4]).to have_content("merge_request_1")
+    end
+
+    it "sorts by priority" do
+      click_link "Priority"
+
+      results_list = page.find('.todos-list')
+      expect(results_list.all('p')[0]).to have_content("issue_3")
+      expect(results_list.all('p')[1]).to have_content("merge_request_1")
+      expect(results_list.all('p')[2]).to have_content("issue_1")
+      expect(results_list.all('p')[3]).to have_content("issue_2")
+      expect(results_list.all('p')[4]).to have_content("issue_4")
+    end
+  end
+
+  context 'issues and merge requests' do
+    let(:issue_1) { create(:issue, id: 10000, title: 'issue_1', project: project) }
+    let(:issue_2) { create(:issue, id: 10001, title: 'issue_2', project: project) }
+    let(:merge_request_1) { create(:merge_request, id: 10000, title: 'merge_request_1', source_project: project) }
+
+    before do
+      issue_1.labels << label_1
+      issue_2.labels << label_2
+
+      create(:todo, user: user, project: project, target: issue_1)
+      create(:todo, user: user, project: project, target: issue_2)
+      create(:todo, user: user, project: project, target: merge_request_1)
+
+      login_as(user)
+      visit dashboard_todos_path
+    end
+
+    it "doesn't mix issues and merge requests priorities" do
+      click_link "Priority"
+
+      results_list = page.find('.todos-list')
+      expect(results_list.all('p')[0]).to have_content("issue_1")
+      expect(results_list.all('p')[1]).to have_content("issue_2")
+      expect(results_list.all('p')[2]).to have_content("merge_request_1")
+    end
+  end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0342f4f1d97e31b9dcde976092e1af23354cf940..3ae83ac082d5ea3aff2496704cb2b1176e5607bf 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
   let(:user)    { create(:user) }
   let(:author)  { create(:user) }
   let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
-  let(:issue)   { create(:issue) }
+  let(:issue)   { create(:issue, due_date: Date.today) }
 
   describe 'GET /dashboard/todos' do
     context 'User does not have todos' do
@@ -13,7 +13,7 @@ describe 'Dashboard Todos', feature: true do
         visit dashboard_todos_path
       end
       it 'shows "All done" message' do
-        expect(page).to have_content "You're all done!"
+        expect(page).to have_content "Todos let you see what you should do next."
       end
     end
 
@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
         expect(page).to have_selector('.todos-list .todo', count: 1)
       end
 
+      it 'shows due date as today' do
+        page.within first('.todo') do
+          expect(page).to have_content 'Due today'
+        end
+      end
+
       describe 'deleting the todo' do
         before do
           first('.done-todo').click
@@ -38,7 +44,28 @@ describe 'Dashboard Todos', feature: true do
         end
 
         it 'shows "All done" message' do
-          expect(page).to have_content("You're all done!")
+          expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+        end
+      end
+
+      context 'todo is stale on the page' do
+        before do
+          todos = TodosFinder.new(user, state: :pending).execute
+          TodoService.new.mark_todos_as_done(todos, user)
+        end
+
+        describe 'deleting the todo' do
+          before do
+            first('.done-todo').click
+          end
+
+          it 'is removed from the list' do
+            expect(page).not_to have_selector('.todos-list .todo')
+          end
+
+          it 'shows "All done" message' do
+            expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+          end
         end
       end
     end
@@ -97,6 +124,19 @@ describe 'Dashboard Todos', feature: true do
           expect(page).to have_css("#todo_#{Todo.first.id}")
         end
       end
+
+      describe 'mark all as done', js: true do
+        before do
+          visit dashboard_todos_path
+          click_link('Mark all as done')
+        end
+
+        it 'shows "All done" message!' do
+          expect(page).to have_content 'To do 0'
+          expect(page).to have_content "You're all done!"
+          expect(page).not_to have_selector('.gl-pagination')
+        end
+      end
     end
 
     context 'User has a Todo in a project pending deletion' do
@@ -112,7 +152,7 @@ describe 'Dashboard Todos', feature: true do
         within('.todos-pending-count') { expect(page).to have_content '0' }
         expect(page).to have_content 'To do 0'
         expect(page).to have_content 'Done 0'
-        expect(page).to have_content "You're all done!"
+        expect(page).to have_content "Good job! Looks like you don't have any todos left."
       end
     end
   end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 3cbc8253ad6260d52a4c10d44789b836dfb93f77..72354834c5a539d1bfbd4a983ee0154dfa39efcc 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -12,7 +12,7 @@ describe 'Triggers' do
 
   context 'create a trigger' do
     before do
-      click_on 'Add Trigger'
+      click_on 'Add trigger'
       expect(@project.triggers.count).to eq(1)
     end
 
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index d370f90f7d97b6d14fdc8f8e6faf7352d3805534..b750f27ea72d27b05aa082fe1e508e77c3794dd2 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
   end
 
   def register_u2f_device(u2f_device = nil)
-    u2f_device ||= FakeU2fDevice.new(page)
+    name = FFaker::Name.first_name
+    u2f_device ||= FakeU2fDevice.new(page, name)
     u2f_device.respond_to_u2f_registration
     click_on 'Setup New U2F Device'
     expect(page).to have_content('Your device was successfully set up')
+    fill_in "Pick a name", with: name
     click_on 'Register U2F Device'
     u2f_device
   end
@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
     end
 
     describe 'when 2FA via OTP is enabled' do
-      it 'allows registering a new device' do
+      it 'allows registering a new device with a name' do
         visit profile_account_path
         manage_two_factor_authentication
         expect(page.body).to match("You've already enabled two-factor authentication using mobile")
 
-        register_u2f_device
+        u2f_device = register_u2f_device
 
+        expect(page.body).to match(u2f_device.name)
         expect(page.body).to match('Your U2F device was registered')
       end
 
@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
 
         # First device
         manage_two_factor_authentication
-        register_u2f_device
+        first_device = register_u2f_device
         expect(page.body).to match('Your U2F device was registered')
 
         # Second device
-        manage_two_factor_authentication
-        register_u2f_device
+        second_device = register_u2f_device
         expect(page.body).to match('Your U2F device was registered')
+
+        expect(page.body).to match(first_device.name)
+        expect(page.body).to match(second_device.name)
+        expect(U2fRegistration.count).to eq(2)
+      end
+
+      it 'allows deleting a device' do
+        visit profile_account_path
         manage_two_factor_authentication
-        expect(page.body).to match('You have 2 U2F devices registered')
+        expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+        first_u2f_device = register_u2f_device
+        second_u2f_device = register_u2f_device
+
+        click_on "Delete", match: :first
+
+        expect(page.body).to match('Successfully deleted')
+        expect(page.body).not_to match(first_u2f_device.name)
+        expect(page.body).to match(second_u2f_device.name)
       end
     end
 
@@ -137,10 +156,11 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
 
     describe "when 2FA via OTP is disabled" do
       it "allows logging in with the U2F device" do
+        user.update_attribute(:otp_required_for_login, false)
         login_with(user)
 
         @u2f_device.respond_to_u2f_authentication
-        click_on "Login Via U2F Device"
+        click_on "Sign in via U2F device"
         expect(page.body).to match('We heard back from your U2F device')
         click_on "Authenticate via U2F Device"
 
@@ -154,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
         login_with(user)
 
         @u2f_device.respond_to_u2f_authentication
-        click_on "Login Via U2F Device"
+        click_on "Sign in via U2F device"
         expect(page.body).to match('We heard back from your U2F device')
         click_on "Authenticate via U2F Device"
 
@@ -162,6 +182,19 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
       end
     end
 
+    it 'persists remember_me value via hidden field' do
+      login_with(user, remember: true)
+
+      @u2f_device.respond_to_u2f_authentication
+      click_on "Sign in via U2F device"
+      expect(page.body).to match('We heard back from your U2F device')
+
+      within 'div#js-authenticate-u2f' do
+        field = first('input#user_remember_me', visible: false)
+        expect(field.value).to eq '1'
+      end
+    end
+
     describe "when a given U2F device has already been registered by another user" do
       describe "but not the current user" do
         it "does not allow logging in with that particular device" do
@@ -176,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
           # Try authenticating user with the old U2F device
           login_as(current_user)
           @u2f_device.respond_to_u2f_authentication
-          click_on "Login Via U2F Device"
+          click_on "Sign in via U2F device"
           expect(page.body).to match('We heard back from your U2F device')
           click_on "Authenticate via U2F Device"
 
@@ -197,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
           # Try authenticating user with the same U2F device
           login_as(current_user)
           @u2f_device.respond_to_u2f_authentication
-          click_on "Login Via U2F Device"
+          click_on "Sign in via U2F device"
           expect(page.body).to match('We heard back from your U2F device')
           click_on "Authenticate via U2F Device"
 
@@ -208,10 +241,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
 
     describe "when a given U2F device has not been registered" do
       it "does not allow logging in with that particular device" do
-        unregistered_device = FakeU2fDevice.new(page)
+        unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
         login_as(user)
         unregistered_device.respond_to_u2f_authentication
-        click_on "Login Via U2F Device"
+        click_on "Sign in via U2F device"
         expect(page.body).to match('We heard back from your U2F device')
         click_on "Authenticate via U2F Device"
 
@@ -238,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
         [first_device, second_device].each do |device|
           login_as(user)
           device.respond_to_u2f_authentication
-          click_on "Login Via U2F Device"
+          click_on "Sign in via U2F device"
           expect(page.body).to match('We heard back from your U2F device')
           click_on "Authenticate via U2F Device"
 
@@ -262,6 +295,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
       end
 
       it "deletes u2f registrations" do
+        visit profile_account_path
         expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
       end
     end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..33b52d1547efe38ae4bdd4547b34a2a8697eee93
--- /dev/null
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe 'Unsubscribe links', feature: true do
+  include Warden::Test::Helpers
+
+  let(:recipient) { create(:user) }
+  let(:author) { create(:user) }
+  let(:project) { create(:empty_project, :public) }
+  let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
+  let(:issue) { Issues::CreateService.new(project, author, params).execute }
+
+  let(:mail) { ActionMailer::Base.deliveries.last }
+  let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
+  let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
+  let(:body_link) { body.find_link('unsubscribe')['href'] }
+
+  before do
+    perform_enqueued_jobs { issue }
+  end
+
+  context 'when logged out' do
+    context 'when visiting the link from the body' do
+      it 'shows the unsubscribe confirmation page and redirects to root path when confirming' do
+        visit body_link
+
+        expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+        expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
+        expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
+        expect(issue.subscribed?(recipient)).to be_truthy
+
+        click_link 'Unsubscribe'
+
+        expect(issue.subscribed?(recipient)).to be_falsey
+        expect(current_path).to eq new_user_session_path
+      end
+
+      it 'shows the unsubscribe confirmation page and redirects to root path when canceling' do
+        visit body_link
+
+        expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+        expect(issue.subscribed?(recipient)).to be_truthy
+
+        click_link 'Cancel'
+
+        expect(issue.subscribed?(recipient)).to be_truthy
+        expect(current_path).to eq new_user_session_path
+      end
+    end
+
+    it 'unsubscribes from the issue when visiting the link from the header' do
+      visit header_link
+
+      expect(page).to have_text('unsubscribed')
+      expect(issue.subscribed?(recipient)).to be_falsey
+    end
+  end
+
+  context 'when logged in' do
+    before { login_as(recipient) }
+
+    it 'unsubscribes from the issue when visiting the link from the email body' do
+      visit body_link
+
+      expect(page).to have_text('unsubscribed')
+      expect(issue.subscribed?(recipient)).to be_falsey
+    end
+
+    it 'unsubscribes from the issue when visiting the link from the header' do
+      visit header_link
+
+      expect(page).to have_text('unsubscribed')
+      expect(issue.subscribed?(recipient)).to be_falsey
+    end
+  end
+end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ce7e809ec768ff183598a9471006489bab57de43
--- /dev/null
+++ b/spec/features/users/snippets_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Snippets tab on a user profile', feature: true, js: true do
+  include WaitForAjax
+
+  context 'when the user has snippets' do
+    let(:user) { create(:user) }
+    let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+    before do
+      allow(Snippet).to receive(:default_per_page).and_return(1)
+      visit user_path(user)
+      page.within('.user-profile-nav') { click_link 'Snippets' }
+      wait_for_ajax
+    end
+
+    it_behaves_like 'paginated snippets', remote: true
+  end
+end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index b5a94fe03831fa43bd25a3808cfa9ec323317423..111ca7f7a703a05963cb8f8e26e1aba47afab244 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,15 +1,16 @@
 require 'spec_helper'
 
-feature 'Users', feature: true do
+feature 'Users', feature: true, js: true do
   let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
 
   scenario 'GET /users/sign_in creates a new user account' do
     visit new_user_session_path
+    click_link 'Register'
     fill_in 'new_user_name',     with: 'Name Surname'
     fill_in 'new_user_username', with: 'Great'
     fill_in 'new_user_email',    with: 'name@mail.com'
     fill_in 'new_user_password', with: 'password1234'
-    expect { click_button 'Sign up' }.to change { User.count }.by(1)
+    expect { click_button 'Register' }.to change { User.count }.by(1)
   end
 
   scenario 'Successful user signin invalidates password reset token' do
@@ -31,15 +32,61 @@ feature 'Users', feature: true do
 
   scenario 'Should show one error if email is already taken' do
     visit new_user_session_path
+    click_link 'Register'
     fill_in 'new_user_name',     with: 'Another user name'
     fill_in 'new_user_username', with: 'anotheruser'
     fill_in 'new_user_email',    with: user.email
     fill_in 'new_user_password', with: '12341234'
-    expect { click_button 'Sign up' }.to change { User.count }.by(0)
+    expect { click_button 'Register' }.to change { User.count }.by(0)
     expect(page).to have_text('Email has already been taken')
     expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
   end
 
+  describe 'redirect alias routes' do
+    before { user }
+
+    scenario '/u/user1 redirects to user page' do
+      visit '/u/user1'
+
+      expect(current_path).to eq user_path(user)
+      expect(page).to have_text(user.name)
+    end
+
+    scenario '/u/user1/groups redirects to user groups page' do
+      visit '/u/user1/groups'
+
+      expect(current_path).to eq user_groups_path(user)
+    end
+
+    scenario '/u/user1/projects redirects to user projects page' do
+      visit '/u/user1/projects'
+
+      expect(current_path).to eq user_projects_path(user)
+    end
+  end
+
+  feature 'username validation' do
+    include WaitForAjax
+    let(:loading_icon) { '.fa.fa-spinner' }
+    let(:username_input) { 'new_user_username' }
+
+    before(:each) do
+      visit new_user_session_path
+      click_link 'Register'
+    end
+    scenario 'shows an error border if the username already exists' do
+      fill_in username_input, with: user.username
+      wait_for_ajax
+      expect(find('.username')).to have_css '.gl-field-error-outline'
+    end
+
+    scenario 'doesn\'t show an error border if the username is available' do
+      fill_in username_input, with: 'new-user'
+      wait_for_ajax
+      expect(find('#new_user_username')).not_to have_css '.gl-field-error-outline'
+    end
+  end
+
   def errors_on_page(page)
     page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
   end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8cfea9659cb43ee98c5bf0d57783eb50ce43c713
--- /dev/null
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe AccessRequestsFinder, services: true do
+  let(:user) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:group) { create(:group, :public) }
+
+  before do
+    project.request_access(access_requester)
+    group.request_access(access_requester)
+  end
+
+  shared_examples 'a finder returning access requesters' do |method_name|
+    it 'returns access requesters' do
+      access_requesters = described_class.new(source).public_send(method_name, user)
+
+      expect(access_requesters.size).to eq(1)
+      expect(access_requesters.first).to be_a "#{source.class}Member".constantize
+      expect(access_requesters.first.user).to eq(access_requester)
+    end
+  end
+
+  shared_examples 'a finder returning no results' do |method_name|
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect(described_class.new(source).public_send(method_name, user)).to be_empty
+    end
+  end
+
+  shared_examples 'a finder raising Gitlab::Access::AccessDeniedError' do |method_name|
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect { described_class.new(source).public_send(method_name, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+
+  describe '#execute' do
+    context 'when current user cannot see project access requests' do
+      it_behaves_like 'a finder returning no results', :execute do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a finder returning no results', :execute do
+        let(:source) { group }
+      end
+    end
+
+    context 'when current user can see access requests' do
+      before do
+        project.team << [user, :master]
+        group.add_owner(user)
+      end
+
+      it_behaves_like 'a finder returning access requesters', :execute do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a finder returning access requesters', :execute do
+        let(:source) { group }
+      end
+    end
+  end
+
+  describe '#execute!' do
+    context 'when current user cannot see access requests' do
+      it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+        let(:source) { group }
+      end
+    end
+
+    context 'when current user can see access requests' do
+      before do
+        project.team << [user, :master]
+        group.add_owner(user)
+      end
+
+      it_behaves_like 'a finder returning access requesters', :execute! do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a finder returning access requesters', :execute! do
+        let(:source) { group }
+      end
+    end
+  end
+end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 6fce11de30fb32369557241ee43e6f32f4ba1055..db60c01db0d8591b3a5cce8f15a965d39bb71a79 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -21,7 +21,7 @@ describe BranchesFinder do
         result = branches_finder.execute
 
         recently_updated_branch = repository.branches.max do |a, b|
-          repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+          repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date
         end
 
         expect(result.first.name).to eq(recently_updated_branch.name)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index ec8809e6926c3940f5dba90bc6d6dceaaba2f202..40bccb8e50bb59c1ab4b5c22b794edd1ae044cc9 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -7,8 +7,8 @@ describe IssuesFinder do
   let(:project2) { create(:empty_project) }
   let(:milestone) { create(:milestone, project: project1) }
   let(:label) { create(:label, project: project2) }
-  let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
-  let(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
+  let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
+  let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
   let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
   let!(:label_link) { create(:label_link, label: label, target: issue2) }
 
@@ -127,6 +127,22 @@ describe IssuesFinder do
         end
       end
 
+      context 'filtering by issue term' do
+        let(:params) { { search: 'git' } }
+
+        it 'returns issues with title and description match for search term' do
+          expect(issues).to contain_exactly(issue1, issue2)
+        end
+      end
+
+      context 'filtering by issue iid' do
+        let(:params) { { search: issue3.to_reference } }
+
+        it 'returns issue with iid match' do
+          expect(issues).to contain_exactly(issue3)
+        end
+      end
+
       context 'when the user is unauthorized' do
         let(:search_user) { nil }
 
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index f90a8e007c8e10b3dc212ba7dc2358e1d4929e1f..29a47e005a669313151a1be73d5b71fae22562e9 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do
       context 'if profile visitor is in one of the private group projects' do
         before do
           project = create(:project, :private, group: private_group, name: 'B', path: 'B')
-          project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
+          project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
         end
 
         it 'shows group' do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10cfb66ec1ca2a199ad3c2c454c0f6d33d38e941
--- /dev/null
+++ b/spec/finders/labels_finder_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe LabelsFinder do
+  describe '#execute' do
+    let(:group_1) { create(:group) }
+    let(:group_2) { create(:group) }
+    let(:group_3) { create(:group) }
+
+    let(:project_1) { create(:empty_project, namespace: group_1) }
+    let(:project_2) { create(:empty_project, namespace: group_2) }
+    let(:project_3) { create(:empty_project) }
+    let(:project_4) { create(:empty_project, :public) }
+    let(:project_5) { create(:empty_project, namespace: group_1) }
+
+    let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') }
+    let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
+    let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
+    let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
+
+    let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
+    let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
+    let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
+
+    let(:user) { create(:user) }
+
+    before do
+      create(:label, project: project_3, title: 'Label 3')
+      create(:group_label, group: group_3, title: 'Group Label 4')
+
+      project_1.team << [user, :developer]
+    end
+
+    context 'with no filter' do
+      it 'returns labels from projects the user have access' do
+        group_2.add_developer(user)
+
+        finder = described_class.new(user)
+
+        expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
+      end
+
+      it 'returns labels available if nil title is supplied' do
+        group_2.add_developer(user)
+        # params[:title] will return `nil` regardless whether it is specified
+        finder = described_class.new(user, title: nil)
+
+        expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
+      end
+    end
+
+    context 'filtering by group_id' do
+      it 'returns labels available for any project within the group' do
+        group_1.add_developer(user)
+
+        finder = described_class.new(user, group_id: group_1.id)
+
+        expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
+      end
+    end
+
+    context 'filtering by project_id' do
+      it 'returns labels available for the project' do
+        finder = described_class.new(user, project_id: project_1.id)
+
+        expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
+      end
+    end
+
+    context 'filtering by title' do
+      it 'returns label with that title' do
+        finder = described_class.new(user, title: 'Group Label 2')
+
+        expect(finder.execute).to eq [group_label_2]
+      end
+
+      it 'returns label with title alias' do
+        finder = described_class.new(user, name: 'Group Label 2')
+
+        expect(finder.execute).to eq [group_label_2]
+      end
+
+      it 'returns no labels if empty title is supplied' do
+        finder = described_class.new(user, title: [])
+
+        expect(finder.execute).to be_empty
+      end
+
+      it 'returns no labels if blank title is supplied' do
+        finder = described_class.new(user, title: '')
+
+        expect(finder.execute).to be_empty
+      end
+
+      it 'returns no labels if empty name is supplied' do
+        finder = described_class.new(user, name: [])
+
+        expect(finder.execute).to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index 4f3304f7b6d9fa3e4cbf6cb706c0310527e63ea3..fdce4e714ffdaf9a90eacd3ae6a9d936b7eeda16 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -51,6 +51,28 @@ describe MoveToProjectFinder do
 
         expect(subject.execute(project).to_a).to eq([other_reporter_project])
       end
+
+      it 'returns a page of projects ordered by id in descending order' do
+        stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+        reporter_project.team << [user, :reporter]
+        developer_project.team << [user, :developer]
+        master_project.team << [user, :master]
+
+        expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+      end
+
+      it 'returns projects after the given offset id' do
+        stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+        reporter_project.team << [user, :reporter]
+        developer_project.team << [user, :developer]
+        master_project.team << [user, :master]
+
+        expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+        expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
+        expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
+      end
     end
 
     context 'search' do
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b0811d134faa2ccc60ba70fb2bda82cbb13d72c3
--- /dev/null
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe PipelinesFinder do
+  let(:project) { create(:project) }
+
+  let!(:tag_pipeline)    { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
+  let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
+
+  subject { described_class.new(project).execute(params) }
+
+  describe "#execute" do
+    context 'when a scope is passed' do
+      context 'when scope is nil' do
+        let(:params) { { scope: nil } }
+
+        it 'selects all pipelines' do
+          expect(subject.count).to be 2
+          expect(subject).to include tag_pipeline
+          expect(subject).to include branch_pipeline
+        end
+      end
+
+      context 'when selecting branches' do
+        let(:params) { { scope: 'branches' } }
+
+        it 'excludes tags' do
+          expect(subject).not_to include tag_pipeline
+          expect(subject).to     include branch_pipeline
+        end
+      end
+
+      context 'when selecting tags' do
+        let(:params) { { scope: 'tags' } }
+
+        it 'excludes branches' do
+          expect(subject).to     include tag_pipeline
+          expect(subject).not_to include branch_pipeline
+        end
+      end
+    end
+
+    # Scoping to running will speed up the test as it doesn't hit the FS
+    let(:params) { { scope: 'running' } }
+
+    it 'orders in descending order on ID' do
+      feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
+
+      expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse
+      expect(subject.map(&:id)).to eq expected_ids
+    end
+  end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 7a3a74335e899a0c2dcef9506c2b418970910ec0..13bda5f7c5ad5ea79f6544cfcc14b293f3b60fcb 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -38,7 +38,7 @@ describe ProjectsFinder do
 
       describe 'with private projects' do
         before do
-          private_project.team.add_user(user, Gitlab::Access::MASTER)
+          private_project.add_user(user, Gitlab::Access::MASTER)
         end
 
         it do
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..98b42e264dc23a768c380ee9a9eff550c3d974b1
--- /dev/null
+++ b/spec/finders/tags_finder_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe TagsFinder do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:repository) { project.repository }
+
+  describe '#execute' do
+    context 'sort only' do
+      it 'sorts by name' do
+        tags_finder = described_class.new(repository, {})
+
+        result = tags_finder.execute
+
+        expect(result.first.name).to eq("v1.0.0")
+      end
+
+      it 'sorts by recently_updated' do
+        tags_finder = described_class.new(repository, { sort: 'updated_desc' })
+
+        result = tags_finder.execute
+        recently_updated_tag = repository.tags.max do |a, b|
+          repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date
+        end
+
+        expect(result.first.name).to eq(recently_updated_tag.name)
+      end
+
+      it 'sorts by last_updated' do
+        tags_finder = described_class.new(repository, { sort: 'updated_asc' })
+
+        result = tags_finder.execute
+
+        expect(result.first.name).to eq('v1.0.0')
+      end
+    end
+
+    context 'filter only' do
+      it 'filters tags by name' do
+        tags_finder = described_class.new(repository, { search: '1.0.0' })
+
+        result = tags_finder.execute
+
+        expect(result.first.name).to eq('v1.0.0')
+        expect(result.count).to eq(1)
+      end
+
+      it 'does not find any tags with that name' do
+        tags_finder = described_class.new(repository, { search: 'hey' })
+
+        result = tags_finder.execute
+
+        expect(result.count).to eq(0)
+      end
+    end
+
+    context 'filter and sort' do
+      it 'filters tags by name and sorts by recently_updated' do
+        params = { sort: 'updated_desc', search: 'v1' }
+        tags_finder = described_class.new(repository, params)
+
+        result = tags_finder.execute
+
+        expect(result.first.name).to eq('v1.1.0')
+        expect(result.count).to eq(2)
+      end
+
+      it 'filters tags by name and sorts by last_updated' do
+        params = { sort: 'updated_asc', search: 'v1' }
+        tags_finder = described_class.new(repository, params)
+
+        result = tags_finder.execute
+
+        expect(result.first.name).to eq('v1.0.0')
+        expect(result.count).to eq(2)
+      end
+    end
+  end
+end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f7e7e733cf71f6a8fab2117697a989e3d8baf70d
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+  describe '#execute' do
+    let(:user)          { create(:user) }
+    let(:project)       { create(:empty_project) }
+    let(:finder)        { described_class }
+
+    before { project.team << [user, :developer] }
+
+    describe '#sort' do
+      context 'by date' do
+        let!(:todo1) { create(:todo, user: user, project: project) }
+        let!(:todo2) { create(:todo, user: user, project: project) }
+        let!(:todo3) { create(:todo, user: user, project: project) }
+
+        it 'sorts with oldest created first' do
+          todos = finder.new(user, { sort: 'id_asc' }).execute
+
+          expect(todos.first).to eq(todo1)
+          expect(todos.second).to eq(todo2)
+          expect(todos.third).to eq(todo3)
+        end
+
+        it 'sorts with newest created first' do
+          todos = finder.new(user, { sort: 'id_desc' }).execute
+
+          expect(todos.first).to eq(todo3)
+          expect(todos.second).to eq(todo2)
+          expect(todos.third).to eq(todo1)
+        end
+      end
+
+      it "sorts by priority" do
+        label_1         = create(:label, title: 'label_1', project: project, priority: 1)
+        label_2         = create(:label, title: 'label_2', project: project, priority: 2)
+        label_3         = create(:label, title: 'label_3', project: project, priority: 3)
+
+        issue_1         = create(:issue, title: 'issue_1', project: project)
+        issue_2         = create(:issue, title: 'issue_2', project: project)
+        issue_3         = create(:issue, title: 'issue_3', project: project)
+        issue_4         = create(:issue, title: 'issue_4', project: project)
+        merge_request_1 = create(:merge_request, source_project: project)
+
+        merge_request_1.labels << label_1
+
+        # Covers the case where Todo has more than one label
+        issue_3.labels         << label_1
+        issue_3.labels         << label_3
+
+        issue_2.labels         << label_3
+        issue_1.labels         << label_2
+
+        todo_1 = create(:todo, user: user, project: project, target: issue_4)
+        todo_2 = create(:todo, user: user, project: project, target: issue_2)
+        todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+        todo_4 = create(:todo, user: user, project: project, target: issue_1)
+        todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+        todos = finder.new(user, { sort: 'priority' }).execute
+
+        expect(todos.first).to eq(todo_3)
+        expect(todos.second).to eq(todo_5)
+        expect(todos.third).to eq(todo_4)
+        expect(todos.fourth).to eq(todo_2)
+        expect(todos.fifth).to eq(todo_1)
+      end
+    end
+  end
+end
diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index a49cbfd5160194614615fa7c9df99a5bc701c6d3..0000000000000000000000000000000000000000
--- a/spec/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe TrendingProjectsFinder do
-  let(:user) { build(:user) }
-
-  describe '#execute' do
-    describe 'without an explicit start date' do
-      subject { described_class.new }
-
-      it 'returns the trending projects' do
-        relation = double(:ar_relation)
-
-        allow(subject).to receive(:projects_for)
-          .with(user)
-          .and_return(relation)
-
-        allow(relation).to receive(:trending)
-          .with(an_instance_of(ActiveSupport::TimeWithZone))
-      end
-    end
-
-    describe 'with an explicit start date' do
-      let(:date) { 2.months.ago }
-
-      subject { described_class.new }
-
-      it 'returns the trending projects' do
-        relation = double(:ar_relation)
-
-        allow(subject).to receive(:projects_for)
-          .with(user)
-          .and_return(relation)
-
-        allow(relation).to receive(:trending)
-          .with(date)
-      end
-    end
-  end
-end
diff --git a/spec/fixtures/api/schemas/board.json b/spec/fixtures/api/schemas/board.json
new file mode 100644
index 0000000000000000000000000000000000000000..03aca4a3cc0242e70cb8331cc6d866526e066edb
--- /dev/null
+++ b/spec/fixtures/api/schemas/board.json
@@ -0,0 +1,11 @@
+{
+  "type": "object",
+  "required" : [
+    "id"
+  ],
+  "properties" : {
+    "id": { "type": "integer" },
+    "name": { "type": "string" }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/boards.json b/spec/fixtures/api/schemas/boards.json
new file mode 100644
index 0000000000000000000000000000000000000000..117564ef77a84840a7ff6e22cb951ba2d0c3fa36
--- /dev/null
+++ b/spec/fixtures/api/schemas/boards.json
@@ -0,0 +1,4 @@
+{
+  "type": "array",
+  "items": { "$ref": "board.json" }
+}
diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json
new file mode 100644
index 0000000000000000000000000000000000000000..a947783d505c59690b3c085feedd349b9cfcf83e
--- /dev/null
+++ b/spec/fixtures/api/schemas/conflicts.json
@@ -0,0 +1,137 @@
+{
+  "type": "object",
+  "required": [
+    "commit_message",
+    "commit_sha",
+    "source_branch",
+    "target_branch",
+    "files"
+  ],
+  "properties": {
+    "commit_message": {"type": "string"},
+    "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
+    "source_branch": {"type": "string"},
+    "target_branch": {"type": "string"},
+    "files": {
+      "type": "array",
+      "items": {
+        "oneOf": [
+          { "$ref": "#/definitions/conflict-text-with-sections" },
+          { "$ref": "#/definitions/conflict-text-for-editor" }
+        ]
+      }
+    }
+  },
+  "definitions": {
+    "conflict-base": {
+      "type": "object",
+      "required": [
+        "old_path",
+        "new_path",
+        "blob_icon",
+        "blob_path"
+      ],
+      "properties": {
+        "old_path": {"type": "string"},
+        "new_path": {"type": "string"},
+        "blob_icon": {"type": "string"},
+        "blob_path": {"type": "string"}
+      }
+    },
+    "conflict-text-for-editor": {
+      "allOf": [
+        {"$ref": "#/definitions/conflict-base"},
+        {
+          "type": "object",
+          "required": [
+            "type",
+            "content_path"
+          ],
+          "properties": {
+            "type": {"type": {"enum": ["text-editor"]}},
+            "content_path": {"type": "string"}
+          }
+        }
+      ]
+    },
+    "conflict-text-with-sections": {
+      "allOf": [
+        {"$ref": "#/definitions/conflict-base"},
+        {
+          "type": "object",
+          "required": [
+            "type",
+            "content_path",
+            "sections"
+          ],
+          "properties": {
+            "type": {"type": {"enum": ["text"]}},
+            "content_path": {"type": "string"},
+            "sections": {
+              "type": "array",
+              "items": {
+                "oneOf": [
+                  { "$ref": "#/definitions/section-context" },
+                  { "$ref": "#/definitions/section-conflict" }
+                ]
+              }
+            }
+          }
+        }
+      ]
+    },
+    "section-base": {
+      "type": "object",
+      "required": [
+        "conflict",
+        "lines"
+      ],
+      "properties": {
+        "conflict": {"type": "boolean"},
+        "lines": {
+          "type": "array",
+          "items": {
+            "type": "object",
+            "required": [
+              "old_line",
+              "new_line",
+              "text",
+              "rich_text"
+            ],
+            "properties": {
+              "type": {"type": "string"},
+              "old_line": {"type": "string"},
+              "new_line": {"type": "string"},
+              "text": {"type": "string"},
+              "rich_text": {"type": "string"}
+            }
+          }
+        }
+      }
+    },
+    "section-context": {
+      "allOf": [
+        {"$ref": "#/definitions/section-base"},
+        {
+          "type": "object",
+          "properties": {
+            "conflict": {"enum": [false]}
+          }
+        }
+      ]
+    },
+    "section-conflict": {
+      "allOf": [
+        {"$ref": "#/definitions/section-base"},
+        {
+          "type": "object",
+          "required": ["id"],
+          "properties": {
+            "conflict": {"enum": [true]},
+            "id": {"type": "string"}
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
new file mode 100644
index 0000000000000000000000000000000000000000..77f2bcee1f348cee55b564c26e651cfb23212331
--- /dev/null
+++ b/spec/fixtures/api/schemas/issue.json
@@ -0,0 +1,50 @@
+{
+  "type": "object",
+  "required" : [
+    "iid",
+    "title",
+    "confidential"
+  ],
+  "properties" : {
+    "iid": { "type": "integer" },
+    "title": { "type": "string" },
+    "confidential": { "type": "boolean" },
+    "due_date": { "type": ["date", "null"] },
+    "labels": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "required": [
+          "id",
+          "color",
+          "description",
+          "title",
+          "priority"
+        ],
+        "properties": {
+          "id": { "type": "integer" },
+          "color": {
+            "type": "string",
+            "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+          },
+          "description": { "type": ["string", "null"] },
+          "text_color": {
+            "type": "string",
+            "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+          },
+          "title": { "type": "string" },
+          "priority": { "type": ["integer", "null"] }
+        },
+        "additionalProperties": false
+      }
+    },
+    "assignee": {
+      "id": { "type": "integet" },
+      "name": { "type": "string" },
+      "username": { "type": "string" },
+      "avatar_url": { "type": "uri" }
+    },
+    "subscribed": { "type": ["boolean", "null"] }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
new file mode 100644
index 0000000000000000000000000000000000000000..70771b21c969d119e7ebf6397d16b9c0c9c31fc0
--- /dev/null
+++ b/spec/fixtures/api/schemas/issues.json
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "required" : [
+    "issues",
+    "size"
+  ],
+  "properties" : {
+    "issues": {
+      "type": "array",
+      "items": { "$ref": "issue.json" }
+    },
+    "size": { "type": "integer" }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
new file mode 100644
index 0000000000000000000000000000000000000000..8d94cf26ecbc9b9805b055520d09702727cbc48f
--- /dev/null
+++ b/spec/fixtures/api/schemas/list.json
@@ -0,0 +1,39 @@
+{
+  "type": "object",
+  "required" : [
+    "id",
+    "list_type",
+    "title",
+    "position"
+  ],
+  "properties" : {
+    "id": { "type": "integer" },
+    "list_type": {
+      "type": "string",
+      "enum": ["backlog", "label", "done"]
+    },
+    "label": {
+      "type": ["object", "null"],
+      "required": [
+        "id",
+        "color",
+        "description",
+        "title",
+        "priority"
+      ],
+      "properties": {
+        "id": { "type": "integer" },
+        "color": {
+          "type": "string",
+          "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+        },
+        "description": { "type": ["string", "null"] },
+        "title": { "type": "string" },
+        "priority": { "type": ["integer", "null"] }
+      }
+    },
+    "title": { "type": "string" },
+    "position": { "type": ["integer", "null"] }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/lists.json b/spec/fixtures/api/schemas/lists.json
new file mode 100644
index 0000000000000000000000000000000000000000..9f618aa9de5cb8014f5ec5b5ee8a0eb99b32740e
--- /dev/null
+++ b/spec/fixtures/api/schemas/lists.json
@@ -0,0 +1,4 @@
+{
+  "type": "array",
+  "items": { "$ref": "list.json" }
+}
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
new file mode 100644
index 0000000000000000000000000000000000000000..712f6f797b47f241138749dd9df5ca8612627998
--- /dev/null
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -0,0 +1,41 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Cool!
+
+/close
+/todo
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml
new file mode 100644
index 0000000000000000000000000000000000000000..2d2e2f94290bd7da8f9071e23668737cd3fea629
--- /dev/null
+++ b/spec/fixtures/emails/commands_only_reply.eml
@@ -0,0 +1,39 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+/close
+/todo
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_incoming_email_token.eml
similarity index 100%
rename from spec/fixtures/emails/wrong_authentication_token.eml
rename to spec/fixtures/emails/wrong_incoming_email_token.eml
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 73f5470cf358bb276f19ebee38b9aa11ba91686b..c706e418d267ac60772e86ea3a0327fbb9662999 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -218,42 +218,24 @@ describe ApplicationHelper do
     end
 
     it 'includes a default js-timeago class' do
-      expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
+      expect(element.attr('class')).to eq 'js-timeago'
     end
 
     it 'accepts a custom html_class' do
       expect(element(html_class: 'custom_class').attr('class')).
-        to eq 'js-timeago custom_class js-timeago-pending'
+        to eq 'js-timeago custom_class'
     end
 
     it 'accepts a custom tooltip placement' do
       expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
     end
 
-    it 're-initializes timeago Javascript' do
-      el = element.next_element
-
-      expect(el.name).to eq 'script'
-      expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
-    end
-
-    it 'allows the script tag to be excluded' do
-      expect(element(skip_js: true)).not_to include 'script'
-    end
-
     it 'converts to Time' do
       expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
     end
 
-    it 'add class for the short format and includes inline script' do
+    it 'add class for the short format' do
       timeago_element = element(short_format: 'short')
-      expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending'
-      script_element = timeago_element.next_element
-      expect(script_element.name).to eq 'script'
-    end
-
-    it 'add class for the short format and does not include inline script' do
-      timeago_element = element(short_format: 'short', skip_js: true)
       expect(timeago_element.attr('class')).to eq 'js-short-timeago'
       expect(timeago_element.next_element).to eq nil
     end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 94972eed945b274f61a72a9694b4bd003cb4d2d0..a43a7238c708b1ac9a5ba87d02e6e9ba5f61602a 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -69,18 +69,40 @@ describe BlobHelper do
   end
 
   describe "#edit_blob_link" do
-    let(:project) { create(:project) }
+    let(:namespace) { create(:namespace, name: 'gitlab' )}
+    let(:project) { create(:project, namespace: namespace) }
 
     before do
       allow(self).to receive(:current_user).and_return(double)
+      allow(self).to receive(:can_collaborate_with_project?).and_return(true)
     end
 
     it 'verifies blob is text' do
-      expect(self).not_to receive(:blob_text_viewable?)
+      expect(helper).not_to receive(:blob_text_viewable?)
 
       button = edit_blob_link(project, 'refs/heads/master', 'README.md')
 
       expect(button).to start_with('<button')
     end
+
+    it 'uses the passed blob instead retrieve from repository' do
+      blob = project.repository.blob_at('refs/heads/master', 'README.md')
+
+      expect(project.repository).not_to receive(:blob_at)
+
+      edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
+    end
+
+    it 'returns a link with the proper route' do
+      link = edit_blob_link(project, 'master', 'README.md')
+
+      expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
+    end
+
+    it 'returns a link with the passed link_opts on the expected route' do
+      link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
+
+      expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
+    end
   end
 end
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 157cc4665a2b61d7f033b5dec2a47f3a55f67a17..c6e3c5c2368caddc87d3bd14811a989c03dedcb7 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do
     end
 
     it 'includes the current message' do
-      current = double(message: 'Current Message')
+      current = BroadcastMessage.new(message: 'Current Message')
 
       allow(helper).to receive(:broadcast_message_style).and_return(nil)
 
@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do
     end
 
     it 'includes custom style' do
-      current = double(message: 'Current Message')
+      current = BroadcastMessage.new(message: 'Current Message')
 
       allow(helper).to receive(:broadcast_message_style).and_return('foo')
 
diff --git a/spec/helpers/components_helper_spec.rb b/spec/helpers/components_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94a59193be846dee42d3647fc65cc0fd50443e22
--- /dev/null
+++ b/spec/helpers/components_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe ComponentsHelper do
+  describe '#gitlab_workhorse_version' do
+    context 'without a Gitlab-Workhorse header' do
+      it 'shows the version from Gitlab::Workhorse.version' do
+        expect(helper.gitlab_workhorse_version).to eq Gitlab::Workhorse.version
+      end
+    end
+
+    context 'with a Gitlab-Workhorse header' do
+      before do
+        helper.request.headers['Gitlab-Workhorse'] = '42.42.0-rc3'
+      end
+
+      it 'shows the actual GitLab Workhorse version currently in use' do
+        expect(helper.gitlab_workhorse_version).to eq '42.42.0'
+      end
+    end
+  end
+end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 9c7c79f57c6310e3e4e1338c67a87bf6569871e9..837e7afa7e8e401056b07d09aa6f9cb9417d6940 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -61,7 +61,7 @@ describe DiffHelper do
 
   describe '#diff_line_content' do
     it 'returns non breaking space when line is empty' do
-      expect(diff_line_content(nil)).to eq(' &nbsp;')
+      expect(diff_line_content(nil)).to eq('&nbsp;')
     end
 
     it 'returns the line itself' do
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 022aba0c0d079946ad05e6749cd9491370616107..594b40303bc50793ec1632de1960d4529e1a748c 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -62,4 +62,21 @@ describe EventsHelper do
       expect(helper.event_note(input)).to eq(expected)
     end
   end
+
+  describe '#event_commit_title' do
+    let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
+    subject { helper.event_commit_title(message) }
+
+    it "returns the first line, truncated to 70 chars" do
+      is_expected.to eq(message[0..66] + "...")
+    end
+
+    it "is not html-safe" do
+      is_expected.not_to be_a(ActiveSupport::SafeBuffer)
+    end
+
+    it "handles empty strings" do
+      expect(helper.event_commit_title("")).to eq("")
+    end
+  end
 end
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b1ef1e05a2daaae5e6520238cdcf03b5d148569
--- /dev/null
+++ b/spec/helpers/git_helper_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe GitHelper do
+  describe '#short_sha' do
+    let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') }
+
+    it { expect(short_sha).to eq('d4e043f6') }
+  end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0807534720a9b69a6b1812692b5de048bd2710dc..233d00534e507446288b4c577f43e5b6be49c1ba 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -18,4 +18,67 @@ describe GroupsHelper do
       expect(group_icon(group.path)).to match('group_avatar.png')
     end
   end
+
+  describe 'group_lfs_status' do
+    let(:group) { create(:group) }
+    let!(:project) { create(:empty_project, namespace_id: group.id) }
+
+    before do
+      allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+    end
+
+    context 'only one project in group' do
+      before do
+        group.update_attribute(:lfs_enabled, true)
+      end
+
+      it 'returns all projects as enabled' do
+        expect(group_lfs_status(group)).to include('Enabled for all projects')
+      end
+
+      it 'returns all projects as disabled' do
+        project.update_attribute(:lfs_enabled, false)
+
+        expect(group_lfs_status(group)).to include('Enabled for 0 out of 1 project')
+      end
+    end
+
+    context 'more than one project in group' do
+      before do
+        create(:empty_project, namespace_id: group.id)
+      end
+
+      context 'LFS enabled in group' do
+        before do
+          group.update_attribute(:lfs_enabled, true)
+        end
+
+        it 'returns both projects as enabled' do
+          expect(group_lfs_status(group)).to include('Enabled for all projects')
+        end
+
+        it 'returns only one as enabled' do
+          project.update_attribute(:lfs_enabled, false)
+
+          expect(group_lfs_status(group)).to include('Enabled for 1 out of 2 projects')
+        end
+      end
+
+      context 'LFS disabled in group' do
+        before do
+          group.update_attribute(:lfs_enabled, false)
+        end
+
+        it 'returns both projects as disabled' do
+          expect(group_lfs_status(group)).to include('Disabled for all projects')
+        end
+
+        it 'returns only one as disabled' do
+          project.update_attribute(:lfs_enabled, true)
+
+          expect(group_lfs_status(group)).to include('Disabled for 1 out of 2 projects')
+        end
+      end
+    end
+  end
 end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index 3391234e9f5c5f0c362d2a9fa0fcdedad0fb7954..187b891b9273c2b85f121af71e9866bcfe33c882 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -1,6 +1,30 @@
 require 'rails_helper'
 
 describe ImportHelper do
+  describe '#import_project_target' do
+    let(:user) { create(:user) }
+
+    before do
+      allow(helper).to receive(:current_user).and_return(user)
+    end
+
+    context 'when current user can create namespaces' do
+      it 'returns project namespace' do
+        user.update_attribute(:can_create_group, true)
+
+        expect(helper.import_project_target('asd', 'vim')).to eq 'asd/vim'
+      end
+    end
+
+    context 'when current user can not create namespaces' do
+      it "takes the current user's namespace" do
+        user.update_attribute(:can_create_group, false)
+
+        expect(helper.import_project_target('asd', 'vim')).to eq "#{user.namespace_path}/vim"
+      end
+    end
+  end
+
   describe '#github_project_link' do
     context 'when provider does not specify a custom URL' do
       it 'uses default GitHub URL' do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62cc10f579a534b43beec52598cee59d5e43d3e7
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe IssuablesHelper do
+  let(:label)  { build_stubbed(:label) }
+  let(:label2) { build_stubbed(:label) }
+
+  describe '#issuable_labels_tooltip' do
+    it 'returns label text' do
+      expect(issuable_labels_tooltip([label])).to eq(label.title)
+    end
+
+    it 'returns label text' do
+      expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
+    end
+  end
+
+  describe '#issuables_state_counter_text' do
+    let(:user) { create(:user) }
+
+    describe 'state text' do
+      before do
+        allow(helper).to receive(:issuables_count_for_state).and_return(42)
+      end
+
+      it 'returns "Open" when state is :opened' do
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+      end
+
+      it 'returns "Closed" when state is :closed' do
+        expect(helper.issuables_state_counter_text(:issues, :closed)).
+          to eq('<span>Closed</span> <span class="badge">42</span>')
+      end
+
+      it 'returns "Merged" when state is :merged' do
+        expect(helper.issuables_state_counter_text(:merge_requests, :merged)).
+          to eq('<span>Merged</span> <span class="badge">42</span>')
+      end
+
+      it 'returns "All" when state is :all' do
+        expect(helper.issuables_state_counter_text(:merge_requests, :all)).
+          to eq('<span>All</span> <span class="badge">42</span>')
+      end
+    end
+
+    describe 'counter caching based on issuable type and params', :caching do
+      let(:params) do
+        {
+          scope: 'created-by-me',
+          state: 'opened',
+          utf8: '✓',
+          author_id: '11',
+          assignee_id: '18',
+          label_name: ['bug', 'discussion', 'documentation'],
+          milestone_title: 'v4.0',
+          sort: 'due_date_asc',
+          namespace_id: 'gitlab-org',
+          project_id: 'gitlab-ce',
+          page: 2
+        }.with_indifferent_access
+      end
+
+      it 'returns the cached value when called for the same issuable type & with the same params' do
+        expect(helper).to receive(:params).twice.and_return(params)
+        expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+
+        expect(helper).not_to receive(:issuables_count_for_state)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+      end
+
+      it 'does not take some keys into account in the cache key' do
+        expect(helper).to receive(:params).and_return({
+          author_id: '11',
+          state: 'foo',
+          sort: 'foo',
+          utf8: 'foo',
+          page: 'foo'
+        }.with_indifferent_access)
+        expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+
+        expect(helper).to receive(:params).and_return({
+          author_id: '11',
+          state: 'bar',
+          sort: 'bar',
+          utf8: 'bar',
+          page: 'bar'
+        }.with_indifferent_access)
+        expect(helper).not_to receive(:issuables_count_for_state)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+      end
+
+      it 'does not take params order into account in the cache key' do
+        expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
+        expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+
+        expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
+        expect(helper).not_to receive(:issuables_count_for_state)
+
+        expect(helper.issuables_state_counter_text(:issues, :opened)).
+          to eq('<span>Open</span> <span class="badge">42</span>')
+      end
+    end
+  end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 5e4655dfc95f5dd8f4cfcc042841fe1fb2a0e1e0..abe08d95eced9bcf29aa172cef6b7ae0916efc72 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -62,6 +62,42 @@ describe IssuesHelper do
     it { is_expected.to eq("!1, !2, or !3") }
   end
 
+  describe '#award_user_list' do
+    it "returns a comma-separated list of the first X users" do
+      user = build_stubbed(:user, name: 'Joe')
+      awards = Array.new(3, build_stubbed(:award_emoji, user: user))
+
+      expect(award_user_list(awards, nil, limit: 3))
+        .to eq('Joe, Joe, and Joe')
+    end
+
+    it "displays the current user's name as 'You'" do
+      user = build_stubbed(:user, name: 'Joe')
+      award = build_stubbed(:award_emoji, user: user)
+
+      expect(award_user_list([award], user)).to eq('You')
+      expect(award_user_list([award], nil)).to eq 'Joe'
+    end
+
+    it "truncates lists" do
+      user = build_stubbed(:user, name: 'Jane')
+      awards = Array.new(5, build_stubbed(:award_emoji, user: user))
+
+      expect(award_user_list(awards, nil, limit: 3))
+        .to eq('Jane, Jane, Jane, and 2 more.')
+    end
+
+    it "displays the current user in front of other users" do
+      current_user = build_stubbed(:user)
+      my_award = build_stubbed(:award_emoji, user: current_user)
+      award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
+      awards = Array.new(5, award).push(my_award)
+
+      expect(award_user_list(awards, current_user, limit: 2)).
+        to eq("You, Jane, and 4 more.")
+    end
+  end
+
   describe '#award_active_class' do
     let!(:upvote) { create(:award_emoji) }
 
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 501f150cfda8a3494212648a239e24c2a727a0e5..d30daf4754323d5427cc579b1a130775d104a39e 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -5,27 +5,26 @@ describe LabelsHelper do
     let(:project) { create(:empty_project) }
     let(:label) { create(:label, project: project) }
 
-    context 'with @project set' do
-      before do
-        @project = project
-      end
-
-      it 'uses the instance variable' do
-        expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name%5B%5D=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>}
+    context 'without subject' do
+      it "uses the label's project" do
+        expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
       end
     end
 
-    context 'without @project set' do
-      it "uses the label's project" do
-        expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+    context 'with a project as subject' do
+      let(:namespace) { build(:namespace, name: 'foo3') }
+      let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') }
+
+      it 'links to project issues page' do
+        expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
       end
     end
 
-    context 'with a project argument' do
-      let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') }
+    context 'with a group as subject' do
+      let(:group) { build(:group, name: 'bar') }
 
-      it 'links to merge requests page' do
-        expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+      it 'links to group issues page' do
+        expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
       end
     end
 
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 7998209b7b00e7a759eade60dfa2e42ed37e7990..6703d88e3574ff1a9851bab0327b62344543b48d 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -11,7 +11,7 @@ describe MembersHelper do
 
   describe '#remove_member_message' do
     let(:requester) { build(:user) }
-    let(:project) { create(:project) }
+    let(:project) { create(:empty_project, :public) }
     let(:project_member) { build(:project_member, project: project) }
     let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
     let(:project_member_request) { project.request_access(requester) }
@@ -32,7 +32,7 @@ describe MembersHelper do
 
   describe '#remove_member_title' do
     let(:requester) { build(:user) }
-    let(:project) { create(:project) }
+    let(:project) { create(:empty_project, :public) }
     let(:project_member) { build(:project_member, project: project) }
     let(:project_member_request) { project.request_access(requester) }
     let(:group) { create(:group) }
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28c2268f8d08a7ca3b5e0058ab7fb6b99284a273
--- /dev/null
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe MilestonesHelper do
+  describe '#milestone_counts' do
+    let(:project) { FactoryGirl.create(:project) }
+    let(:counts) { helper.milestone_counts(project.milestones) }
+
+    context 'when there are milestones' do
+      let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+      let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+      let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) }
+
+      it 'returns the correct counts' do
+        expect(counts).to eq(opened: 2, closed: 1, all: 3)
+      end
+    end
+
+    context 'when there are only milestones of one type' do
+      let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+      let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+
+      it 'returns the correct counts' do
+        expect(counts).to eq(opened: 2, closed: 0, all: 2)
+      end
+    end
+
+    context 'when there are no milestones' do
+      it 'returns the correct counts' do
+        expect(counts).to eq(opened: 0, closed: 0, all: 0)
+      end
+    end
+  end
+end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
deleted file mode 100644
index e4d18d8bfc6f2421286ed9b7a37a382b41c8760d..0000000000000000000000000000000000000000
--- a/spec/helpers/nav_helper_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-# Specs in this file have access to a helper object that includes
-# the NavHelper. For example:
-#
-# describe NavHelper do
-#   describe "string concat" do
-#     it "concats two strings with spaces" do
-#       expect(helper.concat_strings("this","that")).to eq("this that")
-#     end
-#   end
-# end
-describe NavHelper do
-  describe '#nav_menu_collapsed?' do
-    it 'returns true when the nav is collapsed in the cookie' do
-      helper.request.cookies[:collapsed_nav] = 'true'
-      expect(helper.nav_menu_collapsed?).to eq true
-    end
-
-    it 'returns false when the nav is not collapsed in the cookie' do
-      helper.request.cookies[:collapsed_nav] = 'false'
-      expect(helper.nav_menu_collapsed?).to eq false
-    end
-  end
-end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index cf632f594c74e37dcf7164174ea267da3a18b63c..dc07657e101354d98fd46db4c59776f18656dfb9 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -97,5 +97,14 @@ describe PageLayoutHelper do
         expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
       end
     end
+
+    it 'escapes content' do
+      allow(helper).to receive(:page_card_attributes)
+        .and_return(foo: %q{foo" http-equiv="refresh}.html_safe)
+
+      tags = helper.page_card_meta_tags
+
+      expect(tags).to include(%q{content="foo&quot; http-equiv=&quot;refresh"})
+    end
   end
 end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 604204cca0a4c3a6ce760694bd4101d50990fde0..8113742923b05969c4af4d22338f2e8317548938 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,7 +11,7 @@ describe ProjectsHelper do
 
   describe "can_change_visibility_level?" do
     let(:project) { create(:project) }
-    let(:user) { create(:user) }
+    let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
     let(:fork_project) { Projects::ForkService.new(project, user).execute }
 
     it "returns false if there are no appropriate permissions" do
@@ -72,7 +72,7 @@ describe ProjectsHelper do
       it 'returns an HTML link to the user' do
         link = helper.link_to_member(project, user)
 
-        expect(link).to match(%r{/u/#{user.username}})
+        expect(link).to match(%r{/#{user.username}})
       end
     end
   end
@@ -136,4 +136,86 @@ describe ProjectsHelper do
       expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
     end
   end
+
+  describe '#last_push_event' do
+    let(:user) { double(:user, fork_of: nil) }
+    let(:project) { double(:project, id: 1) }
+
+    before do
+      allow(helper).to receive(:current_user).and_return(user)
+      helper.instance_variable_set(:@project, project)
+    end
+
+    context 'when there is no current_user' do
+      let(:user) { nil }
+
+      it 'returns nil' do
+        expect(helper.last_push_event).to eq(nil)
+      end
+    end
+
+    it 'returns recent push on the current project' do
+      event = double(:event)
+      expect(user).to receive(:recent_push).with([project.id]).and_return(event)
+
+      expect(helper.last_push_event).to eq(event)
+    end
+
+    context 'when current user has a fork of the current project' do
+      let(:fork) { double(:fork, id: 2) }
+
+      it 'returns recent push considering fork events' do
+        expect(user).to receive(:fork_of).with(project).and_return(fork)
+
+        event_on_fork = double(:event)
+        expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork)
+
+        expect(helper.last_push_event).to eq(event_on_fork)
+      end
+    end
+  end
+
+  describe "#project_feature_access_select" do
+    let(:project) { create(:empty_project, :public) }
+    let(:user)    { create(:user) }
+
+    context "when project is internal or public" do
+      it "shows all options" do
+        helper.instance_variable_set(:@project, project)
+        result = helper.project_feature_access_select(:issues_access_level)
+        expect(result).to include("Disabled")
+        expect(result).to include("Only team members")
+        expect(result).to include("Everyone with access")
+      end
+    end
+
+    context "when project is private" do
+      before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+
+      it "shows only allowed options" do
+        helper.instance_variable_set(:@project, project)
+        result = helper.project_feature_access_select(:issues_access_level)
+        expect(result).to include("Disabled")
+        expect(result).to include("Only team members")
+        expect(result).not_to include("Everyone with access")
+      end
+    end
+
+    context "when project moves from public to private" do
+      before do
+        project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
+        project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+      end
+
+      it "shows the highest allowed level selected" do
+        helper.instance_variable_set(:@project, project)
+        result = helper.project_feature_access_select(:issues_access_level)
+
+        expect(result).to include("Disabled")
+        expect(result).to include("Only team members")
+        expect(result).not_to include("Everyone with access")
+        expect(result).to have_selector('option[selected]', text: "Only team members")
+      end
+    end
+  end
 end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b0bb991539b38ca0fb189c96739f72aafec045c6..64aa41020c9928f186a1a13f82ec14942a76c8af 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,6 +6,38 @@ describe SearchHelper do
     str
   end
 
+  describe 'parsing result' do
+    let(:project) { create(:project) }
+    let(:repository) { project.repository }
+    let(:results) { repository.search_files('feature', 'master') }
+    let(:search_result) { results.first }
+
+    subject { helper.parse_search_result(search_result) }
+
+    it "returns a valid OpenStruct object" do
+      is_expected.to be_an OpenStruct
+      expect(subject.filename).to eq('CHANGELOG')
+      expect(subject.basename).to eq('CHANGELOG')
+      expect(subject.ref).to eq('master')
+      expect(subject.startline).to eq(188)
+      expect(subject.data.lines[2]).to eq("  - Feature: Replace teams with group membership\n")
+    end
+
+    context "when filename has extension" do
+      let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+      it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+      it { expect(subject.basename).to eq('CONTRIBUTE') }
+    end
+
+    context "when file under directory" do
+      let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+      it { expect(subject.filename).to eq('a/b/c.md') }
+      it { expect(subject.basename).to eq('a/b/c') }
+    end
+  end
+
   describe 'search_autocomplete_source' do
     context "with no current user" do
       before do
@@ -32,6 +64,10 @@ describe SearchHelper do
         expect(search_autocomplete_opts("adm").size).to eq(1)
       end
 
+      it "does not allow regular expression in search term" do
+        expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
+      end
+
       it "includes the user's groups" do
         create(:group).add_owner(user)
         expect(search_autocomplete_opts("gro").size).to eq(1)
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d60839b78ecc782a76760e821a7717134596c7ab
--- /dev/null
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe SidekiqHelper do
+  describe 'parse_sidekiq_ps' do
+    it 'parses line with time' do
+      line = '55137	10,0	2,1	S+	2:30pm	sidekiq 4.1.4 gitlab [0 of 25 busy]   '
+      parts = helper.parse_sidekiq_ps(line)
+
+      expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+    end
+
+    it 'parses line with date' do
+      line = '55137	10,0	2,1	S+	Aug 4	sidekiq 4.1.4 gitlab [0 of 25 busy]   '
+      parts = helper.parse_sidekiq_ps(line)
+
+      expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+    end
+
+    it 'parses line with two digit date' do
+      line = '55137	10,0	2,1	S+	Aug 04	sidekiq 4.1.4 gitlab [0 of 25 busy]   '
+      parts = helper.parse_sidekiq_ps(line)
+
+      expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+    end
+
+    it 'parses line with dot as float separator' do
+      line = '55137	10.0	2.1	S+	2:30pm	sidekiq 4.1.4 gitlab [0 of 25 busy]   '
+      parts = helper.parse_sidekiq_ps(line)
+
+      expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+    end
+
+    it 'does fail gracefully on line not matching the format' do
+      line = '55137	10.0	2.1	S+	2:30pm	something'
+      parts = helper.parse_sidekiq_ps(line)
+
+      expect(parts).to eq(['?', '?', '?', '?', '?', '?'])
+    end
+  end
+end
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index bf3ed5c094c3779398955743fce81302455a0a04..21f355853672844661ac90c156333833b6274928 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -19,16 +19,16 @@ describe TimeHelper do
 
   describe "#duration_in_numbers" do
     it "returns minutes and seconds" do
-      duration_in_numbers = {
-        [100, 0] => "01:40",
-        [121, 0] => "02:01",
-        [3721, 0] => "01:02:01",
-        [0, 0] => "00:00",
-        [nil, Time.now.to_i - 42] => "00:42"
+      durations_and_expectations = {
+        100 => "01:40",
+        121 => "02:01",
+        3721 => "01:02:01",
+        0 => "00:00",
+        42 => "00:42"
       }
 
-      duration_in_numbers.each do |interval, expectation|
-        expect(duration_in_numbers(*interval)).to eq(expectation)
+      durations_and_expectations.each do |duration, expectation|
+        expect(duration_in_numbers(duration)).to eq(expectation)
       end
     end
   end
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..90388929612708671adf1a1ab47c0f2135dd2b48
--- /dev/null
+++ b/spec/javascripts/.eslintrc
@@ -0,0 +1,11 @@
+{
+  "plugins": ["jasmine"],
+  "env": {
+    "jasmine": true
+  },
+  "extends": "plugin:jasmine/recommended",
+  "rules": {
+    "prefer-arrow-callback": 0,
+    "func-names": 0
+  }
+}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..a3171353bfb5817e71a03b1e943abe31f8c6da47
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,42 @@
+/* eslint-disable */
+/*= require abuse_reports */
+
+/*= require jquery */
+
+((global) => {
+  const FIXTURE = 'abuse_reports.html';
+  const MAX_MESSAGE_LENGTH = 500;
+
+  function assertMaxLength($message) {
+    expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+  }
+
+  describe('Abuse Reports', function() {
+    fixture.preload(FIXTURE);
+
+    beforeEach(function() {
+      fixture.load(FIXTURE);
+      new global.AbuseReports();
+    });
+
+    it('should truncate long messages', function() {
+      const $longMessage = $('#long');
+      expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+      assertMaxLength($longMessage);
+    });
+
+    it('should not truncate short messages', function() {
+      const $shortMessage = $('#short');
+      expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+    });
+
+    it('should allow clicking a truncated message to expand and collapse the full message', function() {
+      const $longMessage = $('#long');
+      $longMessage.click();
+      expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+      $longMessage.click();
+      assertMaxLength($longMessage);
+    });
+  });
+
+})(window.gl);
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9d855ef106077d1bfcf07b739de36e9ed424ec7b
--- /dev/null
+++ b/spec/javascripts/activities_spec.js.es6
@@ -0,0 +1,62 @@
+/* eslint-disable */
+/*= require js.cookie.js */
+/*= require jquery.endless-scroll.js */
+/*= require pager */
+/*= require activities */
+
+(() => {
+  window.gon || (window.gon = {});
+  const fixtureTemplate = 'event_filter.html';
+  const filters = [
+    {
+      id: 'all',
+    }, {
+      id: 'push',
+      name: 'push events',
+    }, {
+      id: 'merged',
+      name: 'merge events',
+    }, {
+      id: 'comments',
+    },{
+      id: 'team',
+    }];
+
+  function getEventName(index) {
+    let filter = filters[index];
+    return filter.hasOwnProperty('name') ? filter.name : filter.id;
+  }
+
+  function getSelector(index) {
+    let filter = filters[index];
+    return `#${filter.id}_event_filter`
+  }
+
+  describe('Activities', () => {
+    beforeEach(() => {
+      fixture.load(fixtureTemplate);
+      new Activities();
+    });
+
+    for(let i = 0; i < filters.length; i++) {
+      ((i) => {
+        describe(`when selecting ${getEventName(i)}`, () => {
+          beforeEach(() => {
+            $(getSelector(i)).click();
+          });
+
+          for(let x = 0; x < filters.length; x++) {
+            ((x) => {
+              let shouldHighlight = i === x;
+              let testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+              it(`${testName} ${getEventName(x)}`, () => {
+                expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
+              });
+            })(x);
+          }
+        });
+      })(i);
+    }
+  });
+})();
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
index b48026c3b773950e5e58e6816a8909e4719234d1..16e908f3a8150c113dfc561a3a24b247e5213adf 100644
--- a/spec/javascripts/application_spec.js
+++ b/spec/javascripts/application_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require lib/utils/common_utils */
 
@@ -13,17 +14,21 @@
         gl.utils.preventDisabledButtons();
         isClicked = false;
         $button = $('#test-button');
+        expect($button).toExist();
         $button.click(function() {
           return isClicked = true;
         });
         $button.trigger('click');
         return expect(isClicked).toBe(false);
       });
-      return it('should be on the same page if a disabled link clicked', function() {
-        var locationBeforeLinkClick;
+
+      it('should be on the same page if a disabled link clicked', function() {
+        var locationBeforeLinkClick, $link;
         locationBeforeLinkClick = window.location.href;
         gl.utils.preventDisabledButtons();
-        $('#test-link').click();
+        $link = $('#test-link');
+        expect($link).toExist();
+        $link.click();
         return expect(window.location.href).toBe(locationBeforeLinkClick);
       });
     });
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3ddc163033e4705c73d900967417553d05393577..3d705e1cb2e3d1eeb18914412ecabc6159c4bbba 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,17 +1,12 @@
+/* eslint-disable */
 
 /*= require awards_handler */
-
-
 /*= require jquery */
-
-
-/*= require jquery.cookie */
-
-
+/*= require js.cookie */
 /*= require ./fixtures/emoji_menu */
 
 (function() {
-  var awardsHandler, lazyAssert;
+  var awardsHandler, lazyAssert, urlRoot;
 
   awardsHandler = null;
 
@@ -27,11 +22,13 @@
   };
 
   gon.award_menu_url = '/emojis';
+  urlRoot = gon.relative_url_root;
 
   lazyAssert = function(done, assertFn) {
     return setTimeout(function() {
       assertFn();
       return done();
+    // Maybe jasmine.clock here?
     }, 333);
   };
 
@@ -45,10 +42,14 @@
           return cb();
         };
       })(this));
-      return spyOn(jQuery, 'get').and.callFake(function(req, cb) {
+      spyOn(jQuery, 'get').and.callFake(function(req, cb) {
         return cb(window.emojiMenu);
       });
     });
+    afterEach(function() {
+      // restore original url root value
+      gon.relative_url_root = urlRoot;
+    });
     describe('::showEmojiMenu', function() {
       it('should show emoji menu when Add emoji button clicked', function(done) {
         $('.js-add-award').eq(0).click();
@@ -143,6 +144,52 @@
         return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
       });
     });
+    describe('::addYouToUserList', function() {
+      it('should prepend "You" to the award tooltip', function() {
+        var $thumbsUpEmoji, $votesBlock, awardUrl;
+        awardUrl = awardsHandler.getAwardUrl();
+        $votesBlock = $('.js-awards-block').eq(0);
+        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
+        awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+        $thumbsUpEmoji.tooltip();
+        return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
+      });
+      return it('handles the special case where "You" is not cleanly comma seperated', function() {
+        var $thumbsUpEmoji, $votesBlock, awardUrl;
+        awardUrl = awardsHandler.getAwardUrl();
+        $votesBlock = $('.js-awards-block').eq(0);
+        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji.attr('data-title', 'sam');
+        awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+        $thumbsUpEmoji.tooltip();
+        return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
+      });
+    });
+    describe('::removeYouToUserList', function() {
+      it('removes "You" from the front of the tooltip', function() {
+        var $thumbsUpEmoji, $votesBlock, awardUrl;
+        awardUrl = awardsHandler.getAwardUrl();
+        $votesBlock = $('.js-awards-block').eq(0);
+        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
+        $thumbsUpEmoji.addClass('active');
+        awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+        $thumbsUpEmoji.tooltip();
+        return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
+      });
+      return it('handles the special case where "You" is not cleanly comma seperated', function() {
+        var $thumbsUpEmoji, $votesBlock, awardUrl;
+        awardUrl = awardsHandler.getAwardUrl();
+        $votesBlock = $('.js-awards-block').eq(0);
+        $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+        $thumbsUpEmoji.attr('data-title', 'You and sam');
+        $thumbsUpEmoji.addClass('active');
+        awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+        $thumbsUpEmoji.tooltip();
+        return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
+      });
+    });
     describe('search', function() {
       return it('should filter the emoji', function() {
         $('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 78795f7654a6978d2166281dfae8293ba5c105c7..36254a7370eace88ab74020461e99608a67988dc 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require behaviors/autosize */
 
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 4c52ecd903d6c1b9431423b10003682650972999..7370ccb4a08313a10c6ccd54c5e8e11c72428e96 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require behaviors/quick_submit */
 
@@ -8,6 +9,7 @@
     beforeEach(function() {
       fixture.load('behaviors/quick_submit.html');
       $('form').submit(function(e) {
+        // Prevent a form submit from moving us off the testing page
         return e.preventDefault();
       });
       return this.spies = {
@@ -38,6 +40,8 @@
       expect($('input[type=submit]')).toBeDisabled();
       return expect($('button[type=submit]')).toBeDisabled();
     });
+    // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
+    // only run the tests that apply to the current platform
     if (navigator.userAgent.match(/Macintosh/)) {
       it('responds to Meta+Enter', function() {
         $('input.quick-submit-input').trigger(keydownEvent());
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 724c3baf98902217d53578dfa95e351999026d65..32469a4fd1fa66427c29be11a115d710aba2b9a2 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require behaviors/requires_input */
 
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..b84dfc8197beb764d80ee799c20570466f1b9cd0
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -0,0 +1,171 @@
+/* eslint-disable */
+//= require jquery
+//= require jquery_ujs
+//= require js.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('Store', () => {
+  beforeEach(() => {
+    Vue.http.interceptors.push(boardsMockInterceptor);
+    gl.boardService = new BoardService('/test/issue-boards/board', '1');
+    gl.issueBoards.BoardsStore.create();
+
+    Cookies.set('issue_board_welcome_hidden', 'false', {
+      expires: 365 * 10,
+      path: ''
+    });
+  });
+
+  afterEach(() => {
+    Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+  });
+
+  it('starts with a blank state', () => {
+    expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+  });
+
+  describe('lists', () => {
+    it('creates new list without persisting to DB', () => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+    });
+
+    it('finds list by ID', () => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+      const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+      expect(list.id).toBe(1);
+    });
+
+    it('finds list by type', () => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+      const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+
+      expect(list).toBeDefined();
+    });
+
+    it('finds list limited by type', () => {
+      gl.issueBoards.BoardsStore.addList({
+        id: 1,
+        position: 0,
+        title: 'Test',
+        list_type: 'backlog'
+      });
+      const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+
+      expect(list).toBeDefined();
+    });
+
+    it('gets issue when new list added', (done) => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+      const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+      setTimeout(() => {
+        expect(list.issues.length).toBe(1);
+        expect(list.issues[0].id).toBe(1);
+        done();
+      }, 0);
+    });
+
+    it('persists new list', (done) => {
+      gl.issueBoards.BoardsStore.new({
+        title: 'Test',
+        type: 'label',
+        label: {
+          id: 1,
+          title: 'Testing',
+          color: 'red',
+          description: 'testing;'
+        }
+      });
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+      setTimeout(() => {
+        const list = gl.issueBoards.BoardsStore.findList('id', 1);
+        expect(list).toBeDefined();
+        expect(list.id).toBe(1);
+        expect(list.position).toBe(0);
+        done();
+      }, 0);
+    });
+
+    it('check for blank state adding', () => {
+      expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+    });
+
+    it('check for blank state not adding', () => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+      expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+    });
+
+    it('check for blank state adding when backlog & done list exist', () => {
+      gl.issueBoards.BoardsStore.addList({
+        list_type: 'backlog'
+      });
+      gl.issueBoards.BoardsStore.addList({
+        list_type: 'done'
+      });
+
+      expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+    });
+
+    it('adds the blank state', () => {
+      gl.issueBoards.BoardsStore.addBlankState();
+
+      const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+      expect(list).toBeDefined();
+    });
+
+    it('removes list from state', () => {
+      gl.issueBoards.BoardsStore.addList(listObj);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+      gl.issueBoards.BoardsStore.removeList(1, 'label');
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+    });
+
+    it('moves the position of lists', () => {
+      const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+            listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+      gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+
+      expect(listOne.position).toBe(1);
+    });
+
+    it('moves an issue from one list to another', (done) => {
+      const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+            listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+      setTimeout(() => {
+        expect(listOne.issues.length).toBe(1);
+        expect(listTwo.issues.length).toBe(1);
+
+        gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+
+        expect(listOne.issues.length).toBe(0);
+        expect(listTwo.issues.length).toBe(1);
+
+        done();
+      }, 0);
+    });
+  });
+});
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..90cb89265451cd45f35a6ef0e88c77ace7e20074
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -0,0 +1,84 @@
+/* eslint-disable */
+//= require jquery
+//= require jquery_ujs
+//= require js.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('Issue model', () => {
+  let issue;
+
+  beforeEach(() => {
+    gl.boardService = new BoardService('/test/issue-boards/board', '1');
+    gl.issueBoards.BoardsStore.create();
+
+    issue = new ListIssue({
+      title: 'Testing',
+      iid: 1,
+      confidential: false,
+      labels: [{
+        id: 1,
+        title: 'test',
+        color: 'red',
+        description: 'testing'
+      }]
+    });
+  });
+
+  it('has label', () => {
+    expect(issue.labels.length).toBe(1);
+  });
+
+  it('add new label', () => {
+    issue.addLabel({
+      id: 2,
+      title: 'bug',
+      color: 'blue',
+      description: 'bugs!'
+    });
+    expect(issue.labels.length).toBe(2);
+  });
+
+  it('does not add existing label', () => {
+    issue.addLabel({
+      id: 2,
+      title: 'test',
+      color: 'blue',
+      description: 'bugs!'
+    });
+
+    expect(issue.labels.length).toBe(1);
+  });
+
+  it('finds label', () => {
+    const label = issue.findLabel(issue.labels[0]);
+    expect(label).toBeDefined();
+  });
+
+  it('removes label', () => {
+    const label = issue.findLabel(issue.labels[0]);
+    issue.removeLabel(label);
+    expect(issue.labels.length).toBe(0);
+  });
+
+  it('removes multiple labels', () => {
+    issue.addLabel({
+      id: 2,
+      title: 'bug',
+      color: 'blue',
+      description: 'bugs!'
+    });
+    expect(issue.labels.length).toBe(2);
+
+    issue.removeLabels([issue.labels[0], issue.labels[1]]);
+    expect(issue.labels.length).toBe(0);
+  });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..dfbcbe3a7c1db031e9ce1fda89d700ac050167e1
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -0,0 +1,86 @@
+/* eslint-disable */
+//= require jquery
+//= require jquery_ujs
+//= require js.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('List model', () => {
+  let list;
+
+  beforeEach(() => {
+    Vue.http.interceptors.push(boardsMockInterceptor);
+    gl.boardService = new BoardService('/test/issue-boards/board', '1');
+    gl.issueBoards.BoardsStore.create();
+
+    list = new List(listObj);
+  });
+
+  afterEach(() => {
+    Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+  });
+
+  it('gets issues when created', (done) => {
+    setTimeout(() => {
+      expect(list.issues.length).toBe(1);
+      done();
+    }, 0);
+  });
+
+  it('saves list and returns ID', (done) => {
+    list = new List({
+      title: 'test',
+      label: {
+        id: 1,
+        title: 'test',
+        color: 'red'
+      }
+    });
+    list.save();
+
+    setTimeout(() => {
+      expect(list.id).toBe(1);
+      expect(list.type).toBe('label');
+      expect(list.position).toBe(0);
+      done();
+    }, 0);
+  });
+
+  it('destroys the list', (done) => {
+    gl.issueBoards.BoardsStore.addList(listObj);
+    list = gl.issueBoards.BoardsStore.findList('id', 1);
+    expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+    list.destroy();
+
+    setTimeout(() => {
+      expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+      done();
+    }, 0);
+  });
+
+  it('gets issue from list', (done) => {
+    setTimeout(() => {
+      const issue = list.findIssue(1);
+      expect(issue).toBeDefined();
+      done();
+    }, 0);
+  });
+
+  it('removes issue', (done) => {
+    setTimeout(() => {
+      const issue = list.findIssue(1);
+      expect(list.issues.length).toBe(1);
+      list.removeIssue(issue);
+      expect(list.issues.length).toBe(0);
+      done();
+    }, 0);
+  });
+});
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..fcb3d8f17d8c6326ce8e216b0b2db4d478f36956
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -0,0 +1,57 @@
+/* eslint-disable */
+const listObj = {
+  id: 1,
+  position: 0,
+  title: 'Test',
+  list_type: 'label',
+  label: {
+    id: 1,
+    title: 'Testing',
+    color: 'red',
+    description: 'testing;'
+  }
+};
+
+const listObjDuplicate = {
+  id: 2,
+  position: 1,
+  title: 'Test',
+  list_type: 'label',
+  label: {
+    id: 2,
+    title: 'Testing',
+    color: 'red',
+    description: 'testing;'
+  }
+};
+
+const BoardsMockData = {
+  'GET': {
+    '/test/issue-boards/board/1/lists{/id}/issues': {
+      issues: [{
+        title: 'Testing',
+        iid: 1,
+        confidential: false,
+        labels: []
+      }],
+      size: 1
+    }
+  },
+  'POST': {
+    '/test/issue-boards/board/1/lists{/id}': listObj
+  },
+  'PUT': {
+    '/test/issue-boards/board/1/lists{/id}': {}
+  },
+  'DELETE': {
+    '/test/issue-boards/board/1/lists{/id}': {}
+  }
+};
+
+const boardsMockInterceptor = (request, next) => {
+  const body = BoardsMockData[request.method][request.url];
+
+  next(request.respondWith(JSON.stringify(body), {
+    status: 200
+  }));
+};
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..370944b6a8c9f55267f5aa4a30b9ff9c10b614e9
--- /dev/null
+++ b/spec/javascripts/build_spec.js.es6
@@ -0,0 +1,175 @@
+/* global Build */
+/* eslint-disable no-new */
+//= require build
+//= require breakpoints
+//= require jquery.nicescroll
+//= require turbolinks
+
+(() => {
+  describe('Build', () => {
+    fixture.preload('build.html');
+
+    beforeEach(function () {
+      fixture.load('build.html');
+      spyOn($, 'ajax');
+    });
+
+    describe('constructor', () => {
+      beforeEach(function () {
+        jasmine.clock().install();
+      });
+
+      afterEach(() => {
+        jasmine.clock().uninstall();
+      });
+
+      describe('setup', function () {
+        beforeEach(function () {
+          this.build = new Build();
+        });
+
+        it('copies build options', function () {
+          expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2');
+          expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json');
+          expect(this.build.buildStatus).toBe('passed');
+          expect(this.build.buildStage).toBe('test');
+          expect(this.build.state).toBe('buildstate');
+        });
+
+        it('only shows the jobs matching the current stage', function () {
+          expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+          expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+          expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+        });
+
+        it('selects the current stage in the build dropdown menu', function () {
+          expect($('.stage-selection').text()).toBe('test');
+        });
+
+        it('updates the jobs when the build dropdown changes', function () {
+          $('.stage-item:contains("build")').click();
+
+          expect($('.stage-selection').text()).toBe('build');
+          expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+          expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+          expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+        });
+      });
+
+      describe('initial build trace', function () {
+        beforeEach(function () {
+          new Build();
+        });
+
+        it('displays the initial build trace', function () {
+          expect($.ajax.calls.count()).toBe(1);
+          const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
+          expect(url).toBe('http://example.com/root/test-build/builds/2.json');
+          expect(dataType).toBe('json');
+          expect(success).toEqual(jasmine.any(Function));
+
+          success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
+
+          expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
+        });
+
+        it('removes the spinner', function () {
+          const [{ success, context }] = $.ajax.calls.argsFor(0);
+          success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
+
+          expect($('.js-build-refresh').length).toBe(0);
+        });
+      });
+
+      describe('running build', function () {
+        beforeEach(function () {
+          $('.js-build-options').data('buildStatus', 'running');
+          this.build = new Build();
+          spyOn(this.build, 'location')
+            .and.returnValue('http://example.com/root/test-build/builds/2');
+        });
+
+        it('updates the build trace on an interval', function () {
+          jasmine.clock().tick(4001);
+
+          expect($.ajax.calls.count()).toBe(2);
+          let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
+          expect(url).toBe(
+            'http://example.com/root/test-build/builds/2/trace.json?state=buildstate'
+          );
+          expect(dataType).toBe('json');
+          expect(success).toEqual(jasmine.any(Function));
+
+          success.call(context, {
+            html: '<span>Update<span>',
+            status: 'running',
+            state: 'newstate',
+            append: true,
+          });
+
+          expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+          expect(this.build.state).toBe('newstate');
+
+          jasmine.clock().tick(4001);
+
+          expect($.ajax.calls.count()).toBe(3);
+          [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
+          expect(url).toBe(
+            'http://example.com/root/test-build/builds/2/trace.json?state=newstate'
+          );
+          expect(dataType).toBe('json');
+          expect(success).toEqual(jasmine.any(Function));
+
+          success.call(context, {
+            html: '<span>More</span>',
+            status: 'running',
+            state: 'finalstate',
+            append: true,
+          });
+
+          expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+          expect(this.build.state).toBe('finalstate');
+        });
+
+        it('replaces the entire build trace', function () {
+          jasmine.clock().tick(4001);
+          let [{ success, context }] = $.ajax.calls.argsFor(1);
+          success.call(context, {
+            html: '<span>Update</span>',
+            status: 'running',
+            append: true,
+          });
+
+          expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+          jasmine.clock().tick(4001);
+          [{ success, context }] = $.ajax.calls.argsFor(2);
+          success.call(context, {
+            html: '<span>Different</span>',
+            status: 'running',
+            append: false,
+          });
+
+          expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+          expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+        });
+
+        it('reloads the page when the build is done', function () {
+          spyOn(Turbolinks, 'visit');
+
+          jasmine.clock().tick(4001);
+          const [{ success, context }] = $.ajax.calls.argsFor(1);
+          success.call(context, {
+            html: '<span>Final</span>',
+            status: 'passed',
+            append: true,
+          });
+
+          expect(Turbolinks.visit).toHaveBeenCalledWith(
+            'http://example.com/root/test-build/builds/2'
+          );
+        });
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..93f73fa0e9a669a5a69525f954888bd7a2b3abdd
--- /dev/null
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -0,0 +1,39 @@
+/* eslint-disable */
+/*= require sidebar */
+/*= require jquery */
+/*= require js.cookie */
+/*= require lib/utils/text_utility */
+
+((global) => {
+  describe('Dashboard', () => {
+    const fixtureTemplate = 'dashboard.html';
+
+    function todosCountText() {
+      return $('.js-todos-count').text();
+    }
+
+    function triggerToggle(newCount) {
+      $(document).trigger('todo:toggle', newCount);
+    }
+
+    fixture.preload(fixtureTemplate);
+    beforeEach(() => {
+      fixture.load(fixtureTemplate);
+      new global.Sidebar();
+    });
+
+    it('should update todos-count after receiving the todo:toggle event', () => {
+      triggerToggle(5);
+      expect(todosCountText()).toEqual('5');
+    });
+
+    it('should display todos-count with delimiter', () => {
+      triggerToggle(1000);
+      expect(todosCountText()).toEqual('1,000');
+
+      triggerToggle(1000000);
+      expect(todosCountText()).toEqual('1,000,000');
+    });
+  });
+
+})(window.gl);
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
deleted file mode 100644
index 6b9617341fe7b34dc86fe374ebfa2ccb9bea4ee6..0000000000000000000000000000000000000000
--- a/spec/javascripts/datetime_utility_spec.js.coffee
+++ /dev/null
@@ -1,31 +0,0 @@
-#= require lib/utils/datetime_utility
-
-describe 'Date time utils', ->
-  describe 'get day name', ->
-    it 'should return Sunday', ->
-      day = gl.utils.getDayName(new Date('07/17/2016'))
-      expect(day).toBe('Sunday')
-
-    it 'should return Monday', ->
-      day = gl.utils.getDayName(new Date('07/18/2016'))
-      expect(day).toBe('Monday')
-
-    it 'should return Tuesday', ->
-      day = gl.utils.getDayName(new Date('07/19/2016'))
-      expect(day).toBe('Tuesday')
-
-    it 'should return Wednesday', ->
-      day = gl.utils.getDayName(new Date('07/20/2016'))
-      expect(day).toBe('Wednesday')
-
-    it 'should return Thursday', ->
-      day = gl.utils.getDayName(new Date('07/21/2016'))
-      expect(day).toBe('Thursday')
-
-    it 'should return Friday', ->
-      day = gl.utils.getDayName(new Date('07/22/2016'))
-      expect(day).toBe('Friday')
-
-    it 'should return Saturday', ->
-      day = gl.utils.getDayName(new Date('07/23/2016'))
-      expect(day).toBe('Saturday')
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9fdbab3a9e983d1d47e25de61e00e723c7a5ed7b
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -0,0 +1,65 @@
+/* eslint-disable */
+//= require lib/utils/datetime_utility
+(() => {
+  describe('Date time utils', () => {
+    describe('get day name', () => {
+      it('should return Sunday', () => {
+        const day = gl.utils.getDayName(new Date('07/17/2016'));
+        expect(day).toBe('Sunday');
+      });
+
+      it('should return Monday', () => {
+        const day = gl.utils.getDayName(new Date('07/18/2016'));
+        expect(day).toBe('Monday');
+      });
+
+      it('should return Tuesday', () => {
+        const day = gl.utils.getDayName(new Date('07/19/2016'));
+        expect(day).toBe('Tuesday');
+      });
+
+      it('should return Wednesday', () => {
+        const day = gl.utils.getDayName(new Date('07/20/2016'));
+        expect(day).toBe('Wednesday');
+      });
+
+      it('should return Thursday', () => {
+        const day = gl.utils.getDayName(new Date('07/21/2016'));
+        expect(day).toBe('Thursday');
+      });
+
+      it('should return Friday', () => {
+        const day = gl.utils.getDayName(new Date('07/22/2016'));
+        expect(day).toBe('Friday');
+      });
+
+      it('should return Saturday', () => {
+        const day = gl.utils.getDayName(new Date('07/23/2016'));
+        expect(day).toBe('Saturday');
+      });
+    });
+
+    describe('get day difference', () => {
+      it('should return 7', () => {
+        const firstDay = new Date('07/01/2016');
+        const secondDay = new Date('07/08/2016');
+        const difference = gl.utils.getDayDifference(firstDay, secondDay);
+        expect(difference).toBe(7);
+      });
+
+      it('should return 31', () => {
+        const firstDay = new Date('07/01/2016');
+        const secondDay = new Date('08/01/2016');
+        const difference = gl.utils.getDayDifference(firstDay, secondDay);
+        expect(difference).toBe(31);
+      });
+
+      it('should return 365', () => {
+        const firstDay = new Date('07/02/2015');
+        const secondDay = new Date('07/01/2016');
+        const difference = gl.utils.getDayDifference(firstDay, secondDay);
+        expect(difference).toBe(365);
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9b2845af608835168b08f339011546690d17b6fd
--- /dev/null
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -0,0 +1,122 @@
+/* eslint-disable */
+//= require vue
+//= require diff_notes/models/discussion
+//= require diff_notes/models/note
+//= require diff_notes/stores/comments
+(() => {
+  function createDiscussion(noteId = 1, resolved = true) {
+    CommentsStore.create('a', noteId, true, resolved, 'test');
+  };
+
+  beforeEach(() => {
+    CommentsStore.state = {};
+  });
+
+  describe('New discussion', () => {
+    it('creates new discussion', () => {
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+      createDiscussion();
+      expect(Object.keys(CommentsStore.state).length).toBe(1);
+    });
+
+    it('creates new note in discussion', () => {
+      createDiscussion();
+      createDiscussion(2);
+
+      const discussion = CommentsStore.state['a'];
+      expect(Object.keys(discussion.notes).length).toBe(2);
+    });
+  });
+
+  describe('Get note', () => {
+    beforeEach(() => {
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+      createDiscussion();
+    });
+
+    it('gets note by ID', () => {
+      const note = CommentsStore.get('a', 1);
+      expect(note).toBeDefined();
+      expect(note.id).toBe(1);
+    });
+  });
+
+  describe('Delete discussion', () => {
+    beforeEach(() => {
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+      createDiscussion();
+    });
+
+    it('deletes discussion by ID', () => {
+      CommentsStore.delete('a', 1);
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+    });
+
+    it('deletes discussion when no more notes', () => {
+      createDiscussion();
+      createDiscussion(2);
+      expect(Object.keys(CommentsStore.state).length).toBe(1);
+      expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+
+      CommentsStore.delete('a', 1);
+      CommentsStore.delete('a', 2);
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+    });
+  });
+
+  describe('Update note', () => {
+    beforeEach(() => {
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+      createDiscussion();
+    });
+
+    it('updates note to be unresolved', () => {
+      CommentsStore.update('a', 1, false, 'test');
+
+      const note = CommentsStore.get('a', 1);
+      expect(note.resolved).toBe(false);
+    });
+  });
+
+  describe('Discussion resolved', () => {
+    beforeEach(() => {
+      expect(Object.keys(CommentsStore.state).length).toBe(0);
+      createDiscussion();
+    });
+
+    it('is resolved with single note', () => {
+      const discussion = CommentsStore.state['a'];
+      expect(discussion.isResolved()).toBe(true);
+    });
+
+    it('is unresolved with 2 notes', () => {
+      const discussion = CommentsStore.state['a'];
+      createDiscussion(2, false);
+
+      expect(discussion.isResolved()).toBe(false);
+    });
+
+    it('is resolved with 2 notes', () => {
+      const discussion = CommentsStore.state['a'];
+      createDiscussion(2);
+
+      expect(discussion.isResolved()).toBe(true);
+    });
+
+    it('resolve all notes', () => {
+      const discussion = CommentsStore.state['a'];
+      createDiscussion(2, false);
+
+      discussion.resolveAllNotes();
+      expect(discussion.isResolved()).toBe(true);
+    });
+
+    it('unresolve all notes', () => {
+      const discussion = CommentsStore.state['a'];
+      createDiscussion(2);
+
+      discussion.unResolveAllNotes();
+      expect(discussion.isResolved()).toBe(false);
+    });
+  });
+})();
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
index eced2f6575d3ad9f3e4211f03d73f7bd732e3907..f28983d77644ef4e4d8a970578f7974a763810f2 100644
--- a/spec/javascripts/extensions/array_spec.js
+++ b/spec/javascripts/extensions/array_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require extensions/array */
 
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index b644344b95a5e411a5e6126a82d43261d11fdb4c..9c361bb08675d795ca43be2e58fa06718521fb3a 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require extensions/jquery */
 
diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..009b68d5d1c5005b2006c5dd0e4707d9160017f0
--- /dev/null
+++ b/spec/javascripts/fixtures/.gitignore
@@ -0,0 +1 @@
+*.html.raw
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2ec302abcb7dc9e58bd4f0b03e993c1582663e34
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.html.haml
@@ -0,0 +1,16 @@
+.abuse-reports
+  .message#long
+    Cat ipsum dolor sit amet, hide head under blanket so no one can see.
+    Gate keepers of hell eat and than sleep on your face but hunt by meowing
+    loudly at 5am next to human slave food dispenser cats go for world
+    domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
+    cat is life chase after silly colored fish toys around the house climb a
+    tree, wait for a fireman jump to fireman then scratch his face fall asleep
+    on the washing machine lies down always hungry so caticus cuteicus. Sit on
+    human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
+    pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
+    blanket so no one can see throwup on your pillow.
+  .message#short
+    Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
+    beauty sleep 18 hours - checked, be fabulous for the rest of the day -
+    checked! for shake treat bag.
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
index d55936ee4f9e61f737c735b8e4212a92288e519e..1ef2e8f862496436a5a60d1cfabd73fd7535093d 100644
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -39,7 +39,7 @@
                     %span.note-role Reporter
                     %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
                       %i.fa.fa-spinner.fa-spin
-                      %i.fa.fa-smile-o
+                      %i.fa.fa-smile-o.link-highlight
                 .js-task-list-container.note-body.is-task-list-enabled
                   .note-text
                     %p Suscipit sunt quia quisquam sed eveniet ipsam.
diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..a2bc81c6be7f93b1dd496d09f1377419eb8728f5
--- /dev/null
+++ b/spec/javascripts/fixtures/build.html.haml
@@ -0,0 +1,57 @@
+.build-page
+  .prepend-top-default
+    .autoscroll-container
+      %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+    #js-build-scroll.scroll-controls
+      %a.btn{href: '#build-trace'}
+        %i.fa.fa-angle-up
+      %a.btn{href: '#down-build-trace'}
+        %i.fa.fa-angle-down
+    %pre.build-trace#build-trace
+      %code.bash.js-build-output
+      %i.fa.fa-refresh.fa-spin.js-build-refresh
+
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+  .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
+    Build
+    %strong #1
+    %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
+      %i.fa.fa-angle-double-right
+  .blocks-container
+    .dropdown.build-dropdown
+      .title Stage
+      %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+        %span.stage-selection More
+        %i.fa.fa-caret-down
+      %ul.dropdown-menu
+        %li
+          %a.stage-item build
+        %li
+          %a.stage-item test
+        %li
+          %a.stage-item deploy
+  .builds-container
+    .build-job{data: {stage: 'build'}}
+      %a{href: 'http://example.com/root/test-build/builds/1'}
+        %i.fa.fa-check
+        %i.fa.fa-check-circle-o
+        %span
+          Setup
+    .build-job{data: {stage: 'test'}}
+      %a{href: 'http://example.com/root/test-build/builds/2'}
+        %i.fa.fa-check
+        %i.fa.fa-check-circle-o
+        %span
+          Tests
+    .build-job{data: {stage: 'deploy'}}
+      %a{href: 'http://example.com/root/test-build/builds/3'}
+        %i.fa.fa-check
+        %i.fa.fa-check-circle-o
+        %span
+          Deploy
+
+.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2',
+  build_url: 'http://example.com/root/test-build/builds/2.json',
+  build_status: 'passed',
+  build_stage: 'test',
+  state1: 'buildstate' }}
diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..cc1f8f15c2188e7705bd56b0b7d1693d7c801a31
--- /dev/null
+++ b/spec/javascripts/fixtures/comments.html.haml
@@ -0,0 +1,21 @@
+.flash-container.timeline-content
+.timeline-icon.hidden-xs.hidden-sm
+  %a.author_link
+    %img
+.timeline-content.timeline-content-form
+  %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
+    .md-area
+      .md-header
+      .md-write-holder
+        .zen-backdrop.div-dropzone-wrapper
+          .div-dropzone-wrapper
+            .div-dropzone.dz-clickable
+              %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
+    .note-form-actions.clearfix
+      %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
+      %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
+        Reopen issue
+      %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
+        Close issue
+      %a.btn.btn-cancel.js-note-discard
+        Discard draft
\ No newline at end of file
diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..32446acfd60aa67dda707d245dafbf51eb066b3a
--- /dev/null
+++ b/spec/javascripts/fixtures/dashboard.html.haml
@@ -0,0 +1,45 @@
+%ul.nav.nav-sidebar
+  %li.home.active
+    %a.dashboard-shortcuts-projects
+      %span
+        Projects
+  %li
+    %a
+      %span
+        Todos
+        %span.count.js-todos-count
+        1
+  %li
+    %a.dashboard-shortcuts-activity
+      %span
+        Activity
+  %li
+    %a
+      %span
+        Groups
+  %li
+    %a
+      %span
+        Milestones
+  %li
+    %a.dashboard-shortcuts-issues
+      %span
+        Issues
+        %span
+        1
+  %li
+    %a.dashboard-shortcuts-merge_requests
+      %span
+        Merge Requests
+  %li
+    %a
+      %span
+        Snippets
+  %li
+    %a
+      %span
+        Help
+  %li
+    %a
+      %span
+        Profile Settings
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 99e3f7247bdfa0aa10bb7016383cb7464cf3ffff..41cf40c29cf4a2be14b80b8f7a9b0e0e701bd49a 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (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>";
 
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..95e248cadf88880eb1069e396a4724d21d3398a3
--- /dev/null
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -0,0 +1,21 @@
+%ul.nav-links.event-filter.scrolling-tabs
+  %li.active
+    %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
+      %span
+        All
+  %li
+    %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"}
+      %span
+        Push events
+  %li
+    %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"}
+      %span
+        Merge events
+  %li
+    %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
+      %span
+        Comments
+  %li
+    %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"}
+      %span
+        Team
\ No newline at end of file
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..a20390c08ee39f4c177fee3472f662c59b2ec1e1
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+  .dropdown.inline
+    %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+      Projects
+      %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+    .dropdown-menu.dropdown-select.dropdown-menu-selectable
+      .dropdown-title
+        %span Go to project
+        %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+          %i.fa.fa-times.dropdown-menu-close-icon
+      .dropdown-input
+        %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+        %i.fa.fa-search.dropdown-input-search
+      .dropdown-content
+      .dropdown-loading
+        %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..69445b61367c201cf050864a6c1e17248e32d108
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_field_errors.html.haml
@@ -0,0 +1,15 @@
+%form.gl-show-field-errors{action: 'submit', method: 'post'}
+  .form-group
+    %input.required-text{required: true, type: 'text'} Text
+  .form-group
+    %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email
+  .form-group
+    %input.password{type: 'password', required: true} Password
+  .form-group
+    %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric
+  .form-group
+    %input.hidden{ type:'hidden' }
+  .form-group
+    %input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate
+  .form-group
+  %input.submit{type: 'submit'} Submit
diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4db2ef604de50a409282f68b294d7e72a1e71199
--- /dev/null
+++ b/spec/javascripts/fixtures/header.html.haml
@@ -0,0 +1,35 @@
+%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class
+  .container-fluid
+    .header-content
+      %button.side-nav-toggle
+      %span.sr-only
+        Toggle navigation
+        %i.fa.fa-bars
+      %button.navbar-toggle
+        %span.sr-only
+        Toggle navigation
+        %i.fa.fa-ellipsis-v
+      .navbar-collapse.collapse
+        %ui.nav.navbar-nav
+          %li.hidden-sm.hidden-xs
+          %li.visible-sm.visible-xs
+          %li
+            %a
+              %i.fa.fa-bell.fa-fw
+              %span.badge.todos-pending-count
+          %li
+            %a
+              %i.fa.fa-plus.fa-fw
+          %li.header-user.dropdown
+            %a
+              %img
+              %span.caret
+            .dropdown-menu-nav
+            .dropdown-menu-align-right
+              %ul
+                %li
+                  %a.profile-link
+                %li
+                  %a
+                %li.divider
+                %li.sign-out-link
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..397bdc85c67d4e2695190eb931c15743f3ebb61f
--- /dev/null
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -0,0 +1,16 @@
+.block.labels
+  .sidebar-collapsed-icon.js-sidebar-labels-tooltip
+  .title.hide-collapsed
+    %a.edit-link.pull-right{ href: "#" }
+      Edit
+  .selectbox.hide-collapsed{ style: "display: none;" }
+    .dropdown
+      %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
+        %span.dropdown-toggle-text
+          Label
+        %i.fa.fa-chevron-down
+      .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+        .dropdown-page-one
+          .dropdown-content
+          .dropdown-loading
+            %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d95eb851421364cdef3d120cf4d4be3b2f4694c4
--- /dev/null
+++ b/spec/javascripts/fixtures/issues.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
+  include JavaScriptFixturesHelpers
+
+  let(:admin) { create(:admin) }
+  let(:project) { create(:project_empty_repo) }
+
+  render_views
+
+  before(:all) do
+    clean_frontend_fixtures('issues/')
+  end
+
+  before(:each) do
+    sign_in(admin)
+  end
+
+  it 'issues/open-issue.html.raw' do |example|
+    render_issue(example.description, create(:issue, project: project))
+  end
+
+  it 'issues/closed-issue.html.raw' do |example|
+    render_issue(example.description, create(:closed_issue, project: project))
+  end
+
+  it 'issues/issue-with-task-list.html.raw' do |example|
+    issue = create(:issue, project: project)
+    issue.update(description: '- [ ] Task List Item')
+    render_issue(example.description, issue)
+  end
+
+  private
+
+  def render_issue(fixture_file_name, issue)
+    get :show,
+      namespace_id: project.namespace.to_param,
+      project_id: project.to_param,
+      id: issue.to_param
+
+    expect(response).to be_success
+    store_frontend_fixture(response, fixture_file_name)
+  end
+end
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
deleted file mode 100644
index 06c2ab1e823a87b97374d1a5d6a2e3a120303591..0000000000000000000000000000000000000000
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-:css
-  .hidden { display: none !important; }
-
-.flash-container.flash-container-page
-  .flash-alert
-  .flash-notice
-
-.status-box.status-box-open Open
-.status-box.status-box-closed.hidden Closed
-%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close
-%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen
-
-.detail-page-description
-  .description.js-task-list-container
-    .wiki
-      %ul.task-list
-        %li.task-list-item
-          %input.task-list-item-checkbox{type: 'checkbox'}
-          Task List Item
-      %textarea.js-task-list-field
-        \- [ ] Task List Item
-
-%form.js-issuable-update{action: '/foo'}
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 84e8d0ba1e4b3d87e825375c6dd32ffd67222a29..4919d77e5a46fa1a2b90b0753777694f8fe4cb65 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -1 +1 @@
-[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
+[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
index 95efaff4b694cb4419db8b227e7955d3bac22da8..d48b77cf0ce2d4ba764fd2dd05e8e6e6557460e7 100644
--- a/spec/javascripts/fixtures/right_sidebar.html.haml
+++ b/spec/javascripts/fixtures/right_sidebar.html.haml
@@ -5,6 +5,10 @@
     %div.block.issuable-sidebar-header
       %a.gutter-toggle.pull-right.js-sidebar-toggle
         %i.fa.fa-angle-double-left
+      %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: "1", issuable_type: "issue", url: "/todos" }}
+        %span.js-issuable-todo-text
+          Add Todo
+        %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden
 
     %form.issuable-context-form
       %div.block.labels
diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json
new file mode 100644
index 0000000000000000000000000000000000000000..62c2387d515806c241514e1790d0f577efc267e9
--- /dev/null
+++ b/spec/javascripts/fixtures/todos.json
@@ -0,0 +1,4 @@
+{
+    "count": 1,
+    "delete_path": "/dashboard/todos/1"
+}
\ No newline at end of file
diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml
index 859e79a6c9ede2cfd1b3090588b987ce8872b18d..779d6429a5fe63fbfb25c3d6e5ab02980b9c75c2 100644
--- a/spec/javascripts/fixtures/u2f/authenticate.html.haml
+++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml
@@ -1 +1 @@
-= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
+= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" }
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..8ba238018cdd4775ba88424f354d0bc73a6c67a8
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,170 @@
+/* eslint-disable */
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+  const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+  const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
+  const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+  const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+  const ARROW_KEYS = {
+    DOWN: 40,
+    UP: 38,
+    ENTER: 13,
+    ESC: 27
+  };
+
+  let remoteCallback;
+
+  let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+    i = i || 0;
+    if (!i) direction = direction.toUpperCase();
+    $('body').trigger({
+      type: 'keydown',
+      which: ARROW_KEYS[direction],
+      keyCode: ARROW_KEYS[direction]
+    });
+    i++;
+    if (i <= steps) {
+      navigateWithKeys(direction, steps, cb, i);
+    } else {
+      cb();
+    }
+  };
+
+  let remoteMock = function remoteMock(data, term, callback) {
+    remoteCallback = callback.bind({}, data);
+  }
+
+  describe('Dropdown', function describeDropdown() {
+    fixture.preload('gl_dropdown.html');
+    fixture.preload('projects.json');
+
+    function initDropDown(hasRemote, isFilterable) {
+      this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+        selectable: true,
+        filterable: isFilterable,
+        data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+        text: (project) => {
+          (project.name_with_namespace || project.name);
+        },
+        id: (project) => {
+          project.id;
+        }
+      });
+    }
+
+    beforeEach(() => {
+      fixture.load('gl_dropdown.html');
+      this.dropdownContainerElement = $('.dropdown.inline');
+      this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+      this.projectsData = fixture.load('projects.json')[0];
+    });
+
+    afterEach(() => {
+      $('body').unbind('keydown');
+      this.dropdownContainerElement.unbind('keyup');
+    });
+
+    it('should open on click', () => {
+      initDropDown.call(this, false);
+      expect(this.dropdownContainerElement).not.toHaveClass('open');
+      this.dropdownButtonElement.click();
+      expect(this.dropdownContainerElement).toHaveClass('open');
+    });
+
+    describe('that is open', () => {
+      beforeEach(() => {
+        initDropDown.call(this, false, false);
+        this.dropdownButtonElement.click();
+      });
+
+      it('should select a following item on DOWN keypress', () => {
+        expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+        let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+        navigateWithKeys('down', randomIndex, () => {
+          expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+          expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+        });
+      });
+
+      it('should select a previous item on UP keypress', () => {
+        expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+        navigateWithKeys('down', (this.projectsData.length - 1), () => {
+          expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+          let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+          navigateWithKeys('up', randomIndex, () => {
+            expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+            expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+          });
+        });
+      });
+
+      it('should click the selected item on ENTER keypress', () => {
+        expect(this.dropdownContainerElement).toHaveClass('open')
+        let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+        navigateWithKeys('down', randomIndex, () => {
+          spyOn(Turbolinks, 'visit').and.stub();
+          navigateWithKeys('enter', null, () => {
+            expect(this.dropdownContainerElement).not.toHaveClass('open');
+            let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+            expect(link).toHaveClass('is-active');
+            let linkedLocation = link.attr('href');
+            if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+          });
+        });
+      });
+
+      it('should close on ESC keypress', () => {
+        expect(this.dropdownContainerElement).toHaveClass('open');
+        this.dropdownContainerElement.trigger({
+          type: 'keyup',
+          which: ARROW_KEYS.ESC,
+          keyCode: ARROW_KEYS.ESC
+        });
+        expect(this.dropdownContainerElement).not.toHaveClass('open');
+      });
+    });
+
+    describe('opened and waiting for a remote callback', () => {
+      beforeEach(() => {
+        initDropDown.call(this, true, true);
+        this.dropdownButtonElement.click();
+      });
+
+      it('should not focus search input while remote task is not complete', ()=> {
+        expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+        remoteCallback();
+        expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+      });
+
+      it('should focus search input after remote task is complete', ()=> {
+        remoteCallback();
+        expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+      });
+
+      it('should focus on input when opening for the second time', ()=> {
+        remoteCallback();
+        this.dropdownContainerElement.trigger({
+          type: 'keyup',
+          which: ARROW_KEYS.ESC,
+          keyCode: ARROW_KEYS.ESC
+        });
+        this.dropdownButtonElement.click();
+        expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+      });
+    });
+
+    describe('input focus with array data', () => {
+      it('should focus input when passing array data to drop down', ()=> {
+        initDropDown.call(this, false, true);
+        this.dropdownButtonElement.click();
+        expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+      });
+    });
+  });
+})();
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..0713e30e485692b48e9ceb5fe9f098c47daf85ce
--- /dev/null
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -0,0 +1,112 @@
+/* eslint-disable */
+//= require jquery
+//= require gl_field_errors
+
+((global) => {
+  fixture.preload('gl_field_errors.html');
+
+  describe('GL Style Field Errors', function() {
+    beforeEach(function() {
+      fixture.load('gl_field_errors.html');
+      const $form = this.$form = $('form.gl-show-field-errors');
+      this.fieldErrors = new global.GlFieldErrors($form);
+    });
+
+    it('should select the correct input elements', function() {
+      expect(this.$form).toBeDefined();
+      expect(this.$form.length).toBe(1);
+      expect(this.fieldErrors).toBeDefined();
+      const inputs = this.fieldErrors.state.inputs;
+      expect(inputs.length).toBe(4);
+    });
+
+    it('should ignore elements with custom error handling', function() {
+      const customErrorFlag = 'gl-field-error-ignore';
+      const customErrorElem = $(`.${customErrorFlag}`);
+
+      expect(customErrorElem.length).toBe(1);
+
+      const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+       return input.inputElement.hasClass(customErrorFlag);
+      });
+      expect(customErrors.length).toBe(0);
+    });
+
+    it('should not show any errors before submit attempt', function() {
+      this.$form.find('.email').val('not-a-valid-email').keyup();
+      this.$form.find('.text-required').val('').keyup();
+      this.$form.find('.alphanumberic').val('?---*').keyup();
+
+      const errorsShown = this.$form.find('.gl-field-error-outline');
+      expect(errorsShown.length).toBe(0);
+    });
+
+    it('should show errors when input valid is submitted', function() {
+      this.$form.find('.email').val('not-a-valid-email').keyup();
+      this.$form.find('.text-required').val('').keyup();
+      this.$form.find('.alphanumberic').val('?---*').keyup();
+
+      this.$form.submit();
+
+      const errorsShown = this.$form.find('.gl-field-error-outline');
+      expect(errorsShown.length).toBe(4);
+    });
+
+    it('should properly track validity state on input after invalid submission attempt', function() {
+      this.$form.submit();
+
+      const emailInputModel = this.fieldErrors.state.inputs[1];
+      const fieldState = emailInputModel.state;
+      const emailInputElement = emailInputModel.inputElement;
+
+      // No input
+      expect(emailInputElement).toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(true);
+      expect(fieldState.valid).toBe(false);
+
+      // Then invalid input
+      emailInputElement.val('not-a-valid-email').keyup();
+      expect(emailInputElement).toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(false);
+      expect(fieldState.valid).toBe(false);
+
+      // Then valid input
+      emailInputElement.val('email@gitlab.com').keyup();
+      expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(false);
+      expect(fieldState.valid).toBe(true);
+
+      // Then invalid input
+      emailInputElement.val('not-a-valid-email').keyup();
+      expect(emailInputElement).toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(false);
+      expect(fieldState.valid).toBe(false);
+
+      // Then empty input
+      emailInputElement.val('').keyup();
+      expect(emailInputElement).toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(true);
+      expect(fieldState.valid).toBe(false);
+
+      // Then valid input
+      emailInputElement.val('email@gitlab.com').keyup();
+      expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+      expect(fieldState.empty).toBe(false);
+      expect(fieldState.valid).toBe(true);
+    });
+
+    it('should properly infer error messages', function() {
+      this.$form.submit();
+      const trackedInputs = this.fieldErrors.state.inputs;
+      const inputHasTitle = trackedInputs[1];
+      const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+      const inputNoTitle = trackedInputs[2];
+      const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+
+      expect(noTitleErrorElem.text()).toBe('This field is required.');
+      expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
+    });
+
+  });
+
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 82ee1954a597e44a337c68e50ae398b08acf889b..8c66c45ba7931640c4b3e9de337f9798b2c5a066 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 //= require graphs/stat_graph_contributors_graph
 
 describe("ContributorsGraph", function () {
@@ -7,7 +8,7 @@ describe("ContributorsGraph", function () {
      expect(ContributorsGraph.prototype.x_domain).toEqual(20)
     })
   })
-  
+
   describe("#set_y_domain", function () {
     it("sets the y_domain", function () {
       ContributorsGraph.set_y_domain([{commits: 30}])
@@ -89,7 +90,7 @@ describe("ContributorsGraph", function () {
 })
 
 describe("ContributorsMasterGraph", function () {
-  
+
   // TODO: fix or remove
   //describe("#process_dates", function () {
     //it("gets and parses dates", function () {
@@ -103,7 +104,7 @@ describe("ContributorsMasterGraph", function () {
       //expect(graph.get_dates).toHaveBeenCalledWith(data)
       //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
     //})
-  //}) 
+  //})
 
   describe("#get_dates", function () {
     it("plucks the date field from data collection", function () {
@@ -124,5 +125,5 @@ describe("ContributorsMasterGraph", function () {
     })
   })
 
-  
+
 })
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 56970e22e347810df05e6e1101e70b6d3b8b604c..920e4ee08922c205e52f4ccbea798fafc2ae274e 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 //= require graphs/stat_graph_contributors_util
 
 describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 4b05d401a428fbd328d7d9fcd7ee091d2227f383..ae2821ecad9a477c436967a5be672156451dff37 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 //= require graphs/stat_graph
 
 describe("StatGraph", function () {
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a859655d8bb0f9c6f989fe1802066f9e02f8442
--- /dev/null
+++ b/spec/javascripts/header_spec.js
@@ -0,0 +1,55 @@
+/* eslint-disable */
+/*= require header */
+/*= require lib/utils/text_utility */
+/*= require jquery */
+
+(function() {
+
+  describe('Header', function() {
+    var todosPendingCount = '.todos-pending-count';
+    var fixtureTemplate = 'header.html';
+
+    function isTodosCountHidden() {
+      return $(todosPendingCount).hasClass('hidden');
+    }
+
+    function triggerToggle(newCount) {
+      $(document).trigger('todo:toggle', newCount);
+    }
+
+    fixture.preload(fixtureTemplate);
+    beforeEach(function() {
+      fixture.load(fixtureTemplate);
+    });
+
+    it('should update todos-pending-count after receiving the todo:toggle event', function() {
+      triggerToggle(5);
+      expect($(todosPendingCount).text()).toEqual('5');
+    });
+
+    it('should hide todos-pending-count when it is 0', function() {
+      triggerToggle(0);
+      expect(isTodosCountHidden()).toEqual(true);
+    });
+
+    it('should show todos-pending-count when it is more than 0', function() {
+      triggerToggle(10);
+      expect(isTodosCountHidden()).toEqual(false);
+    });
+
+    describe('when todos-pending-count is 1000', function() {
+      beforeEach(function() {
+        triggerToggle(1000);
+      });
+
+      it('should show todos-pending-count', function() {
+        expect(isTodosCountHidden()).toEqual(false);
+      });
+
+      it('should add delimiter to todos-pending-count', function() {
+        expect($(todosPendingCount).text()).toEqual('1,000');
+      });
+    });
+  });
+
+}).call(this);
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index dc6231ebb38469ab22b38d64057775ad5ab469e5..949114185cf52db9d78fa6672ed9f408c93f06b2 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,120 +1,163 @@
+/* eslint-disable */
 
 /*= require lib/utils/text_utility */
-
-
 /*= require issue */
 
 (function() {
+  var INVALID_URL = 'http://goesnowhere.nothing/whereami';
+  var $boxClosed, $boxOpen, $btnClose, $btnReopen;
+
+  fixture.preload('issues/closed-issue.html');
+  fixture.preload('issues/issue-with-task-list.html');
+  fixture.preload('issues/open-issue.html');
+
+  function expectErrorMessage() {
+    var $flashMessage = $('div.flash-alert');
+    expect($flashMessage).toExist();
+    expect($flashMessage).toBeVisible();
+    expect($flashMessage).toHaveText('Unable to update this issue at this time.');
+  }
+
+  function expectIssueState(isIssueOpen) {
+    expectVisibility($boxClosed, !isIssueOpen);
+    expectVisibility($boxOpen, isIssueOpen);
+
+    expectVisibility($btnClose, isIssueOpen);
+    expectVisibility($btnReopen, !isIssueOpen);
+  }
+
+  function expectPendingRequest(req, $triggeredButton) {
+    expect(req.type).toBe('PUT');
+    expect(req.url).toBe($triggeredButton.attr('href'));
+    expect($triggeredButton).toHaveProp('disabled', true);
+  }
+
+  function expectVisibility($element, shouldBeVisible) {
+    if (shouldBeVisible) {
+      expect($element).not.toHaveClass('hidden');
+    } else {
+      expect($element).toHaveClass('hidden');
+    }
+  }
+
+  function findElements() {
+      $boxClosed = $('div.status-box-closed');
+      expect($boxClosed).toExist();
+      expect($boxClosed).toHaveText('Closed');
+
+      $boxOpen = $('div.status-box-open');
+      expect($boxOpen).toExist();
+      expect($boxOpen).toHaveText('Open');
+
+      $btnClose = $('.btn-close.btn-grouped');
+      expect($btnClose).toExist();
+      expect($btnClose).toHaveText('Close issue');
+
+      $btnReopen = $('.btn-reopen.btn-grouped');
+      expect($btnReopen).toExist();
+      expect($btnReopen).toHaveText('Reopen issue');
+  }
+
   describe('Issue', function() {
-    return describe('task lists', function() {
-      fixture.preload('issues_show.html');
+    describe('task lists', function() {
+      fixture.load('issues/issue-with-task-list.html');
       beforeEach(function() {
-        fixture.load('issues_show.html');
-        return this.issue = new Issue();
+        this.issue = new Issue();
       });
+
       it('modifies the Markdown field', function() {
         spyOn(jQuery, 'ajax').and.stub();
         $('input[type=checkbox]').attr('checked', true).trigger('change');
-        return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+        expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
       });
-      return it('submits an ajax request on tasklist:changed', function() {
+      
+      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('/foo');
-          return expect(req.data.issue.description).not.toBe(null);
+          expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json');
+          expect(req.data.issue.description).not.toBe(null);
         });
-        return $('.js-task-list-field').trigger('tasklist:changed');
+
+        $('.js-task-list-field').trigger('tasklist:changed');
       });
     });
   });
 
-  describe('reopen/close issue', function() {
-    fixture.preload('issues_show.html');
+  describe('close issue', function() {
     beforeEach(function() {
-      fixture.load('issues_show.html');
-      return this.issue = new Issue();
+      fixture.load('issues/open-issue.html');
+      findElements();
+      this.issue = new Issue();
+
+      expectIssueState(true);
     });
+
     it('closes an issue', function() {
-      var $btnClose, $btnReopen;
       spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expect(req.type).toBe('PUT');
-        expect(req.url).toBe('http://gitlab.com/issues/6/close');
-        return req.success({
+        expectPendingRequest(req, $btnClose);
+        req.success({
           id: 34
         });
       });
-      $btnClose = $('a.btn-close');
-      $btnReopen = $('a.btn-reopen');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose.text()).toBe('Close');
-      expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
       $btnClose.trigger('click');
-      expect($btnReopen).toBeVisible();
-      expect($btnClose).toBeHidden();
-      expect($('div.status-box-closed')).toBeVisible();
-      return expect($('div.status-box-open')).toBeHidden();
+
+      expectIssueState(false);
+      expect($btnClose).toHaveProp('disabled', false);
     });
+
     it('fails to close an issue with success:false', function() {
-      var $btnClose, $btnReopen;
       spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expect(req.type).toBe('PUT');
-        expect(req.url).toBe('http://goesnowhere.nothing/whereami');
-        return req.success({
+        expectPendingRequest(req, $btnClose);
+        req.success({
           saved: false
         });
       });
-      $btnClose = $('a.btn-close');
-      $btnReopen = $('a.btn-reopen');
-      $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose.text()).toBe('Close');
-      expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
+      $btnClose.attr('href', INVALID_URL);
       $btnClose.trigger('click');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose).toBeVisible();
-      expect($('div.status-box-closed')).toBeHidden();
-      expect($('div.status-box-open')).toBeVisible();
-      expect($('div.flash-alert')).toBeVisible();
-      return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+
+      expectIssueState(true);
+      expect($btnClose).toHaveProp('disabled', false);
+      expectErrorMessage();
     });
+
     it('fails to closes an issue with HTTP error', function() {
-      var $btnClose, $btnReopen;
       spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expect(req.type).toBe('PUT');
-        expect(req.url).toBe('http://goesnowhere.nothing/whereami');
-        return req.error();
+        expectPendingRequest(req, $btnClose);
+        req.error();
       });
-      $btnClose = $('a.btn-close');
-      $btnReopen = $('a.btn-reopen');
-      $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose.text()).toBe('Close');
-      expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
+      $btnClose.attr('href', INVALID_URL);
       $btnClose.trigger('click');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose).toBeVisible();
-      expect($('div.status-box-closed')).toBeHidden();
-      expect($('div.status-box-open')).toBeVisible();
-      expect($('div.flash-alert')).toBeVisible();
-      return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+
+      expectIssueState(true);
+      expect($btnClose).toHaveProp('disabled', true);
+      expectErrorMessage();
     });
-    return it('reopens an issue', function() {
-      var $btnClose, $btnReopen;
+  });
+
+  describe('reopen issue', function() {
+    beforeEach(function() {
+      fixture.load('issues/closed-issue.html');
+      findElements();
+      this.issue = new Issue();
+
+      expectIssueState(false);
+    });
+
+    it('reopens an issue', function() {
       spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expect(req.type).toBe('PUT');
-        expect(req.url).toBe('http://gitlab.com/issues/6/reopen');
-        return req.success({
+        expectPendingRequest(req, $btnReopen);
+        req.success({
           id: 34
         });
       });
-      $btnClose = $('a.btn-close');
-      $btnReopen = $('a.btn-reopen');
-      expect($btnReopen.text()).toBe('Reopen');
+
       $btnReopen.trigger('click');
-      expect($btnReopen).toBeHidden();
-      expect($btnClose).toBeVisible();
-      expect($('div.status-box-open')).toBeVisible();
-      return expect($('div.status-box-closed')).toBeHidden();
+
+      expectIssueState(true);
+      expect($btnReopen).toHaveProp('disabled', false);
     });
   });
 
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..49687048eb5ab12f101eb493090c7074cf157b6e
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+/* eslint-disable */
+//= require lib/utils/type_utility
+//= require jquery
+//= require bootstrap
+//= require gl_dropdown
+//= require select2
+//= require jquery.nicescroll
+//= require api
+//= require create_label
+//= require issuable_context
+//= require users_select
+//= require labels_select
+
+(() => {
+  let saveLabelCount = 0;
+  describe('Issue dropdown sidebar', () => {
+    fixture.preload('issue_sidebar_label.html');
+
+    beforeEach(() => {
+      fixture.load('issue_sidebar_label.html');
+      new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+      new LabelsSelect();
+
+      spyOn(jQuery, 'ajax').and.callFake((req) => {
+        const d = $.Deferred();
+        let LABELS_DATA = []
+
+        if (req.url === '/root/test/labels.json') {
+          for (let i = 0; i < 10; i++) {
+            LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+          }
+        } else if (req.url === '/root/test/issues/2.json') {
+          let tmp = []
+          for (let i = 0; i < saveLabelCount; i++) {
+            tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+          }
+          LABELS_DATA = {labels: tmp};
+        }
+
+        d.resolve(LABELS_DATA);
+        return d.promise();
+      });
+    });
+
+    it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+      saveLabelCount = 5;
+      $('.edit-link').get(0).click();
+
+      setTimeout(() => {
+        expect($('.dropdown-content a').length).toBe(10);
+
+        $('.dropdown-content a').each(function (i) {
+          if (i < saveLabelCount) {
+            $(this).get(0).click();
+          }
+        });
+
+        $('.edit-link').get(0).click();
+
+        setTimeout(() => {
+          expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+          done();
+        }, 0);
+      }, 0);
+    });
+
+    it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+      saveLabelCount = 6;
+      $('.edit-link').get(0).click();
+
+      setTimeout(() => {
+        expect($('.dropdown-content a').length).toBe(10);
+
+        $('.dropdown-content a').each(function (i) {
+          if (i < saveLabelCount) {
+            $(this).get(0).click();
+          }
+        });
+
+        $('.edit-link').get(0).click();
+
+        setTimeout(() => {
+          expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+          done();
+        }, 0);
+      }, 0);
+    });
+  });
+})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index e2789571607dd066cfa1db7942fcd467decafadc..e0192a2d624f43d074f164a7edf6e1a4d0fa5600 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require line_highlighter */
 
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 61830d267a9c15ab3157dba3ee95fabd737a1fc1..83d279ab414b3c4ec0d392192245b45f281fa5e1 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require merge_request */
 
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 395032a74167d0415de157814e0234321c79cdf8..6a53c6aa6ac6e6a0fe163dcace59d3b247bc9931 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,7 @@
+/* eslint-disable */
 
 /*= require merge_request_tabs */
+//= require breakpoints
 
 (function() {
   describe('MergeRequestTabs', function() {
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 17b32914ec395fe1150c493fa8d619903485fbcd..91f19aca71938a994c0b5af1d443a5adac554431 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,5 +1,6 @@
-
+/* eslint-disable */
 /*= require merge_request_widget */
+/*= require lib/utils/timeago.js */
 
 (function() {
   describe('MergeRequestWidget', function() {
@@ -8,6 +9,7 @@
       window.notify = function() {};
       this.opts = {
         ci_status_url: "http://sampledomain.local/ci/getstatus",
+        ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus",
         ci_status: "",
         ci_message: {
           normal: "Build {{status}} for \"{{title}}\"",
@@ -20,17 +22,48 @@
         gitlab_icon: "gitlab_logo.png",
         builds_path: "http://sampledomain.local/sampleBuildsPath"
       };
-      this["class"] = new MergeRequestWidget(this.opts);
-      return this.ciStatusData = {
-        "title": "Sample MR title",
-        "sha": "12a34bc5",
-        "status": "success",
-        "coverage": 98
-      };
+      this["class"] = new window.gl.MergeRequestWidget(this.opts);
     });
+
+    describe('getCIEnvironmentsStatus', function() {
+      beforeEach(function() {
+        this.ciEnvironmentsStatusData = [{
+          created_at: '2016-09-12T13:38:30.636Z',
+          environment_id: 1,
+          environment_name: 'env1',
+          external_url: 'https://test-url.com',
+          external_url_formatted: 'test-url.com'
+        }];
+
+        spyOn(jQuery, 'getJSON').and.callFake((req, cb) => {
+          cb(this.ciEnvironmentsStatusData);
+        });
+      });
+
+      it('should call renderEnvironments when the environments property is set', function() {
+         const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+         this.class.getCIEnvironmentsStatus();
+         expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
+       });
+
+       it('should not call renderEnvironments when the environments property is not set', function() {
+         this.ciEnvironmentsStatusData = null;
+         const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+         this.class.getCIEnvironmentsStatus();
+         expect(spy).not.toHaveBeenCalled();
+       });
+    });
+
     return describe('getCIStatus', function() {
       beforeEach(function() {
-        return spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
+        this.ciStatusData = {
+          "title": "Sample MR title",
+          "sha": "12a34bc5",
+          "status": "success",
+          "coverage": 98
+        };
+
+        spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
           return function(req, cb) {
             return cb(_this.ciStatusData);
           };
@@ -61,10 +94,10 @@
         this["class"].getCIStatus(false);
         return expect(spy).not.toHaveBeenCalled();
       });
-      return it('should not display a notification on the first check after the widget has been created', function() {
+      it('should not display a notification on the first check after the widget has been created', function() {
         var spy;
         spy = spyOn(window, 'notify');
-        this["class"] = new MergeRequestWidget(this.opts);
+        this["class"] = new window.gl.MergeRequestWidget(this.opts);
         this["class"].getCIStatus(true);
         return expect(spy).not.toHaveBeenCalled();
       });
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 25d3f5b6c04d73fa0bc4499d1e166b0e1173784f..c092424ec32d1c2d0e4daf6edb94b23ab337458c 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,6 @@
+/* eslint-disable */
 
 /*= require jquery-ui/autocomplete */
-
-
 /*= require new_branch_form */
 
 (function() {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 14dc6bfdfdeb865eedb1fb2ebbea00681b5387ef..2e3a4b66e2d0bf4ba960bec3b1007c869f24f2c3 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,8 +1,8 @@
-
+/* eslint-disable */
 /*= require notes */
-
-
+/*= require autosize */
 /*= require gl_form */
+/*= require lib/utils/text_utility */
 
 (function() {
   window.gon || (window.gon = {});
@@ -12,29 +12,63 @@
   };
 
   describe('Notes', function() {
-    return describe('task lists', function() {
+    describe('task lists', function() {
       fixture.preload('issue_note.html');
+
       beforeEach(function() {
         fixture.load('issue_note.html');
         $('form').on('submit', function(e) {
-          return e.preventDefault();
+          e.preventDefault();
         });
-        return this.notes = new Notes();
+        this.notes = new Notes();
       });
+
       it('modifies the Markdown field', function() {
         $('input[type=checkbox]').attr('checked', true).trigger('change');
-        return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+        expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
       });
-      return it('submits the form on tasklist:changed', function() {
-        var submitted;
-        submitted = false;
+
+      it('submits the form on tasklist:changed', function() {
+        var submitted = false;
         $('form').on('submit', function(e) {
           submitted = true;
-          return e.preventDefault();
+          e.preventDefault();
         });
+
         $('.js-task-list-field').trigger('tasklist:changed');
-        return expect(submitted).toBe(true);
+        expect(submitted).toBe(true);
+      });
+    });
+
+    describe('comments', function() {
+      var commentsTemplate = 'comments.html';
+      var textarea = '.js-note-text';
+      fixture.preload(commentsTemplate);
+
+      beforeEach(function() {
+        fixture.load(commentsTemplate);
+        this.notes = new Notes();
+
+        this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
+        spyOn(this.notes, 'renderNote').and.stub();
+
+        $(textarea).data('autosave', {
+          reset: function() {}
+        });
+
+        $('form').on('submit', function(e) {
+          e.preventDefault();
+          $('.js-main-target-form').trigger('ajax:success');
+        });
       });
+
+      it('autosizes after comment submission', function() {
+        $(textarea).text('This is an example comment note');
+        expect(this.autoSizeSpy).not.toHaveBeenTriggered();
+
+        $('.js-comment-button').click();
+        expect(this.autoSizeSpy).toHaveBeenTriggered();
+      })
     });
   });
 
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index ffe49828492f2f649e60f67544e54631f7217e64..1963857bba3bcc46248afafa1e6ebd2d93031cf1 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,22 +1,11 @@
+/* eslint-disable */
 
 /*= require bootstrap */
-
-
 /*= require select2 */
-
-
 /*= require lib/utils/type_utility */
-
-
 /*= require gl_dropdown */
-
-
 /*= require api */
-
-
 /*= require project_select */
-
-
 /*= require project */
 
 (function() {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 38b3b2653ecaa4e38e84900af291f61b806de949..ef03d1147dee956020f4918161106f658686cdde 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,11 +1,10 @@
+/* eslint-disable */
 
 /*= require right_sidebar */
-
-
 /*= require jquery */
+/*= require js.cookie */
 
-
-/*= require jquery.cookie */
+/*= require extensions/jquery.js */
 
 (function() {
   var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -59,12 +58,27 @@
       $labelsIcon.click();
       return assertSidebarState('expanded');
     });
-    return it('should collapse when the icon arrow clicked while it is floating on page', function() {
+    it('should collapse when the icon arrow clicked while it is floating on page', function() {
       $labelsIcon.click();
       assertSidebarState('expanded');
       $toggle.click();
       return assertSidebarState('collapsed');
     });
+
+    it('should broadcast todo:toggle event when add todo clicked', function() {
+      spyOn(jQuery, 'ajax').and.callFake(function() {
+        var d = $.Deferred();
+        var response = fixture.load('todos.json');
+        d.resolve(response);
+        return d.promise();
+      });
+
+      var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
+
+      $('.js-issuable-todo').click();
+
+      expect(todoToggleSpy.calls.count()).toEqual(1);
+    })
   });
 
 }).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d6777ce04521116b00e07785abdf80d1..29080804960c2893eaed4f4f24fecc1888a273ba 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,20 +1,13 @@
+/* eslint-disable */
 
 /*= require gl_dropdown */
-
-
 /*= require search_autocomplete */
-
-
 /*= require jquery */
-
-
 /*= require lib/utils/common_utils */
-
-
 /*= require lib/utils/type_utility */
-
-
 /*= require fuzzaldrin-plus */
+/*= require turbolinks */
+/*= require jquery.turbolinks */
 
 (function() {
   var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -43,6 +36,8 @@
 
   groupName = 'Gitlab Org';
 
+  // Add required attributes to body before starting the test.
+  // section would be dashboard|group|project
   addBodyAttributes = function(section) {
     var $body;
     if (section == null) {
@@ -64,6 +59,7 @@
     }
   };
 
+  // Mock `gl` object in window for dashboard specific page. App code will need it.
   mockDashboardOptions = function() {
     window.gl || (window.gl = {});
     return window.gl.dashboardOptions = {
@@ -72,6 +68,7 @@
     };
   };
 
+  // Mock `gl` object in window for project specific page. App code will need it.
   mockProjectOptions = function() {
     window.gl || (window.gl = {});
     return window.gl.projectOptions = {
@@ -105,20 +102,20 @@
     a3 = "a[href='" + mrsAssignedToMeLink + "']";
     a4 = "a[href='" + mrsIHaveCreatedLink + "']";
     expect(list.find(a1).length).toBe(1);
-    expect(list.find(a1).text()).toBe(' Issues assigned to me ');
+    expect(list.find(a1).text()).toBe('Issues assigned to me');
     expect(list.find(a2).length).toBe(1);
-    expect(list.find(a2).text()).toBe(" Issues I've created ");
+    expect(list.find(a2).text()).toBe("Issues I've created");
     expect(list.find(a3).length).toBe(1);
-    expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
+    expect(list.find(a3).text()).toBe('Merge requests assigned to me');
     expect(list.find(a4).length).toBe(1);
-    return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
+    return expect(list.find(a4).text()).toBe("Merge requests I've created");
   };
 
   describe('Search autocomplete dropdown', function() {
     fixture.preload('search_autocomplete.html');
     beforeEach(function() {
       fixture.load('search_autocomplete.html');
-      return widget = new SearchAutocomplete;
+      return widget = new gl.SearchAutocomplete;
     });
     it('should show Dashboard specific dropdown menu', function() {
       var list;
@@ -144,7 +141,7 @@
       list = widget.wrap.find('.dropdown-menu').find('ul');
       return assertLinks(list, projectIssuesPath, projectMRsPath);
     });
-    return it('should not show category related menu if there is text in the input', function() {
+    it('should not show category related menu if there is text in the input', function() {
       var link, list;
       addBodyAttributes('project');
       mockProjectOptions();
@@ -154,6 +151,23 @@
       link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
       return expect(list.find(link).length).toBe(0);
     });
+    return it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
+      var ENTER = 13;
+      var DOWN = 40;
+      addBodyAttributes();
+      mockDashboardOptions(true);
+      var submitSpy = spyOnEvent('form', 'submit');
+      widget.searchInput.focus();
+      widget.wrap.trigger($.Event('keydown', { which: DOWN }));
+      var enterKeyEvent = $.Event('keydown', { which: ENTER });
+      widget.searchInput.trigger(enterKeyEvent);
+      // This does not currently catch failing behavior. For security reasons,
+      // browsers will not trigger default behavior (form submit, in this
+      // example) on JavaScript-created keypresses.
+      expect(submitSpy).not.toHaveBeenTriggered();
+      // Does a worse job at capturing the intent of the test, but works.
+      expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
+    });
   });
 
 }).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 7b6b55fe545c385fc2e71a862dd4bbc677732412..1f36a048153b83453619c77c7e3762dbce7985e9 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require shortcuts_issuable */
 
@@ -10,6 +11,7 @@
     });
     return describe('#replyWithSelectedText', function() {
       var stubSelection;
+      // Stub window.getSelection to return the provided String.
       stubSelection = function(text) {
         return window.getSelection = function() {
           return text;
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 7d91ed0f85582d114558f4b077a54fd710188a07..9cb8243ee2cb3f6fc74bd1a5e75085b90cf88d5b 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,21 +1,42 @@
-
+/* eslint-disable */
+// PhantomJS (Teaspoons default driver) doesn't have support for
+// Function.prototype.bind, which has caused confusion.  Use this polyfill to
+// avoid the confusion.
 /*= require support/bind-poly */
 
-
+// You can require your own javascript files here. By default this will include
+// everything in application, however you may get better load performance if you
+// require the specific files that are being used in the spec that tests them.
 /*= require jquery */
-
-
 /*= require jquery.turbolinks */
-
-
 /*= require bootstrap */
-
-
 /*= require underscore */
 
-
+// Teaspoon includes some support files, but you can use anything from your own
+// support path too.
+// require support/jasmine-jquery-1.7.0
+// require support/jasmine-jquery-2.0.0
 /*= require support/jasmine-jquery-2.1.0 */
 
+// require support/sinon
+// require support/your-support-file
+// Deferring execution
+// If you're using CommonJS, RequireJS or some other asynchronous library you can
+// defer execution. Call Teaspoon.execute() after everything has been loaded.
+// Simple example of a timeout:
+// Teaspoon.defer = true
+// setTimeout(Teaspoon.execute, 1000)
+// Matching files
+// By default Teaspoon will look for files that match
+// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path
+// and it'll be included in the default suite automatically. If you want to
+// customize suites, check out the configuration in teaspoon_env.rb
+// Manifest
+// If you'd rather require your spec files manually (to control order for
+// instance) you can disable the suite matcher in the configuration and use this
+// file as a manifest.
+// For more information: http://github.com/modeset/teaspoon
+
 (function() {
 
 
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 4e5dd1e59bf65d5d387710e1eca155aac17b5d45..498f0f06797eff31521a36b49d7090287d8977c1 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require syntax_highlight */
 
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index e008ce956adb5ad65a247c329eb0534e336485fe..024a91f0a8030e60ece0b079ada797b5843677d2 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,16 +1,9 @@
+/* eslint-disable */
 
 /*= require u2f/authenticate */
-
-
 /*= require u2f/util */
-
-
 /*= require u2f/error */
-
-
 /*= require u2f */
-
-
 /*= require ./mock_u2f_device */
 
 (function() {
@@ -29,7 +22,7 @@
       setupButton = this.container.find("#js-login-u2f-device");
       setupMessage = this.container.find("p");
       expect(setupMessage.text()).toContain('Insert your security key');
-      expect(setupButton.text()).toBe('Login Via U2F Device');
+      expect(setupButton.text()).toBe('Sign in via U2F device');
       setupButton.trigger('click');
       inProgressMessage = this.container.find("p");
       expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index ca91a716ba3989102fb2fad0727122e17fbe6074..ad133682fb123c86cbd0abb6aafc62f80e09bf30 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 21c5266c60e72f1c0ddd9265e04161cf5127a4ac..abea76f622f685895bdc08227d2b5696d31d1822 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,16 +1,9 @@
+/* eslint-disable */
 
 /*= require u2f/register */
-
-
 /*= require u2f/util */
-
-
 /*= require u2f/error */
-
-
 /*= require u2f */
-
-
 /*= require ./mock_u2f_device */
 
 (function() {
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 3d680ec8ea3f49da8e083f057db5d37bfdcbb339..65b6e3dce3375a8890ed3054c561a6bea15b9c98 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
 
 /*= require zen_mode */
 
@@ -14,8 +15,10 @@
             return true;
           }
         };
+      // Stub Dropzone.forElement(...).enable()
       });
       this.zen = new ZenMode();
+      // Set this manually because we can't actually scroll the window
       return this.zen.scroll_position = 456;
     });
     describe('on enter', function() {
@@ -60,7 +63,7 @@
     return $('a.js-zen-enter').click();
   };
 
-  exitZen = function() {
+  exitZen = function() { // Ohmmmmmmm
     return $('a.js-zen-leave').click();
   };
 
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index dca7f9975701dd90ccc86d89baba7a3bbf569898..a6d2ea11fcc697c2e619e55692f74fdbc0e04563 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
       expect(doc.at_css('a')['href']).to eq link
     end
 
+    it 'autolinks rdar' do
+      link = 'rdar://localhost.com/blah'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'does not autolink javascript' do
+      link = 'javascript://alert(document.cookie);'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a')).to be_nil
+    end
+
+    it 'does not autolink bad URLs' do
+      link = 'foo://23423:::asdf'
+      doc = filter("See #{link}")
+
+      expect(doc.to_s).to eq("See #{link}")
+    end
+
     it 'does not include trailing punctuation' do
       doc = filter("See #{link}.")
       expect(doc.at_css('a').text).to eq link
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index 593bd6d5cac994d24bda92071a9d7b5b28e81d30..e6c90ad87ee7d744c0fa86738b4a3b1307b4246c 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -65,14 +65,14 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
       expect(reference_filter(act).to_html).to eq exp
     end
 
-    it 'includes a title attribute' do
+    it 'includes no title attribute' do
       doc = reference_filter("See #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq range.reference_title
+      expect(doc.css('a').first.attr('title')).to eq ""
     end
 
     it 'includes default classes' do
       doc = reference_filter("See #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range has-tooltip'
     end
 
     it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index d46d3f1489e40a7c9d9cd8cdddf7843ed7ecfd50..e0f082825515b1335ad4a41122571b9455f9eeae 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -55,7 +55,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
 
     it 'includes a title attribute' do
       doc = reference_filter("See #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq commit.link_title
+      expect(doc.css('a').first.attr('title')).to eq commit.title
     end
 
     it 'escapes the title attribute' do
@@ -67,7 +67,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
 
     it 'includes default classes' do
       doc = reference_filter("See #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit has-tooltip'
     end
 
     it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index b5b38cf0c8c29d77469de11e82703db8ebd4b5df..c8e62f528df37a27153aab084a2782be1a1d094f 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -12,11 +12,16 @@ describe Banzai::Filter::EmojiFilter, lib: true do
     ActionController::Base.asset_host = @original_asset_host
   end
 
-  it 'replaces supported emoji' 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'
   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'
+  end
+
   it 'ignores unsupported emoji' do
     exp = act = '<p>:foo:</p>'
     doc = filter(act)
@@ -28,46 +33,96 @@ describe Banzai::Filter::EmojiFilter, lib: true do
     expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
   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'
+  end
+
   it 'matches at the start of a string' do
     doc = filter(':+1:')
     expect(doc.css('img').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
+  end
+
   it 'matches at the end of a string' do
     doc = filter('This gets a :-1:')
     expect(doc.css('img').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
+  end
+
   it 'matches with adjacent text' do
     doc = filter('+1 (:+1:)')
     expect(doc.css('img').size).to eq 1
   end
 
+  it 'unicode matches with adjacent text' do
+    doc = filter('+1 (👍)')
+    expect(doc.css('img').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
   end
 
+  it 'unicode matches multiple emoji in a row' do
+    doc = filter("'🙈🙉🙊'")
+    expect(doc.css('img').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
+  end
+
   it 'has a title attribute' do
     doc = filter(':-1:')
     expect(doc.css('img').first.attr('title')).to eq ':-1:'
   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
     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
@@ -76,12 +131,26 @@ describe Banzai::Filter::EmojiFilter, lib: true do
     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'
+  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/)
   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'
 
@@ -95,4 +164,18 @@ describe Banzai::Filter::EmojiFilter, lib: true do
     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')
+  end
 end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 953466679e4beb276f65b5a1a6adb06f01d91b6f..fbf7a461fa5d55f8955118666b0d9e75483514ac 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -7,11 +7,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
     IssuesHelper
   end
 
-  let(:project) { create(:jira_project) }
-
-  context 'JIRA issue references' do
-    let(:issue)     { ExternalIssue.new('JIRA-123', project) }
-    let(:reference) { issue.to_reference }
+  shared_examples_for "external issue tracker" do
+    it_behaves_like 'a reference containing an element node'
 
     it 'requires project context' do
       expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
@@ -20,6 +17,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
     %w(pre code a style).each do |elem|
       it "ignores valid references contained inside '#{elem}' element" do
         exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
+
         expect(filter(act).to_html).to eq exp
       end
     end
@@ -33,25 +31,30 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
 
     it 'links to a valid reference' do
       doc = filter("Issue #{reference}")
+      issue_id = doc.css('a').first.attr("data-external-issue")
+
       expect(doc.css('a').first.attr('href'))
-        .to eq helper.url_for_issue(reference, project)
+        .to eq helper.url_for_issue(issue_id, project)
     end
 
     it 'links to the external tracker' do
       doc = filter("Issue #{reference}")
+
       link = doc.css('a').first.attr('href')
+      issue_id = doc.css('a').first.attr("data-external-issue")
 
-      expect(link).to eq "http://jira.example/browse/#{reference}"
+      expect(link).to eq(helper.url_for_issue(issue_id, project))
     end
 
     it 'links with adjacent text' do
       doc = filter("Issue (#{reference}.)")
+
       expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
     end
 
     it 'includes a title attribute' do
       doc = filter("Issue #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+      expect(doc.css('a').first.attr('title')).to include("Issue in #{project.issues_tracker.title}")
     end
 
     it 'escapes the title attribute' do
@@ -64,14 +67,65 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
 
     it 'includes default classes' do
       doc = filter("Issue #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
     end
 
     it 'supports an :only_path context' do
       doc = filter("Issue #{reference}", only_path: true)
+
       link = doc.css('a').first.attr('href')
+      issue_id = doc.css('a').first["data-external-issue"]
+
+      expect(link).to eq helper.url_for_issue(issue_id, project, only_path: true)
+    end
+
+    context 'with RequestStore enabled' do
+      let(:reference_filter) { HTML::Pipeline.new([described_class]) }
+
+      before { allow(RequestStore).to receive(:active?).and_return(true) }
+
+      it 'queries the collection on the first call' do
+        expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
+        expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original
 
-      expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+        not_cached = reference_filter.call("look for #{reference}", { project: project })
+
+        expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
+        expect_any_instance_of(Project).not_to receive(:issue_reference_pattern)
+
+        cached = reference_filter.call("look for #{reference}", { project: project })
+
+        # Links must be the same
+        expect(cached[:output].css('a').first[:href]).to eq(not_cached[:output].css('a').first[:href])
+      end
+    end
+  end
+
+  context "redmine project" do
+    let(:project) { create(:redmine_project) }
+    let(:issue) { ExternalIssue.new("#123", project) }
+    let(:reference) { issue.to_reference }
+
+    it_behaves_like "external issue tracker"
+  end
+
+  context "jira project" do
+    let(:project) { create(:jira_project) }
+    let(:reference) { issue.to_reference }
+
+    context "with right markdown" do
+      let(:issue) { ExternalIssue.new("JIRA-123", project) }
+
+      it_behaves_like "external issue tracker"
+    end
+
+    context "with wrong markdown" do
+      let(:issue) { ExternalIssue.new("#123", project) }
+
+      it "ignores reference" do
+        exp = act = "Issue #{reference}"
+        expect(filter(act).to_html).to eq exp
+      end
     end
   end
 end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 695a5bc6fd4418fe6d40575e4217e670f4de86ab..167397c736bdd8e6b15355411ab3057b926f083b 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -46,4 +46,38 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
       expect(doc.at_css('a')['rel']).to include 'noreferrer'
     end
   end
+
+  context 'for non-lowercase scheme links' do
+    let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
+    let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
+
+    it 'adds rel="nofollow" to external links' do
+      expect(doc_with_http.at_css('a')).to have_attribute('rel')
+      expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+      expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
+      expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+    end
+
+    it 'adds rel="noreferrer" to external links' do
+      expect(doc_with_http.at_css('a')).to have_attribute('rel')
+      expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+      expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
+      expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+    end
+
+    it 'skips internal links' do
+      internal_link = Gitlab.config.gitlab.url + "/sign_in"
+      url = internal_link.gsub(/\Ahttp/, 'HtTp')
+      act = %Q(<a href="#{url}">Login</a>)
+      exp = %Q(<a href="#{internal_link}">Login</a>)
+      expect(filter(act).to_html).to eq(exp)
+    end
+
+    it 'skips relative links' do
+      exp = act = %q(<a href="http_spec/foo.rb">Relative URL</a>)
+      expect(filter(act).to_html).to eq(exp)
+    end
+  end
 end
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f9e6bd609f0a1c7f12073639ae9906125a4b045b
--- /dev/null
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Banzai::Filter::HtmlEntityFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' }
+  let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' }
+
+  it 'converts common entities to their HTML-escaped equivalents' do
+    output = filter(unescaped)
+
+    expect(output).to eq(escaped)
+  end
+
+  it 'does not double-escape' do
+    escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
+    expect(filter(escaped)).to eq(escaped)
+  end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index a005b4990e7937e9affaa6d651a430c98edc12f5..8f0b2db3e8e820755b88343884e8e319d651508b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -22,12 +22,12 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
   end
 
   context 'internal reference' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:reference) { issue.to_reference }
 
     it 'ignores valid references when using non-default tracker' do
-      expect_any_instance_of(described_class).to receive(:find_object).
-        with(project, issue.iid).
-        and_return(nil)
+      allow(project).to receive(:default_issues_tracker?).and_return(false)
 
       exp = act = "Issue #{reference}"
       expect(reference_filter(act).to_html).to eq exp
@@ -54,7 +54,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
 
     it 'includes a title attribute' do
       doc = reference_filter("Issue #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+      expect(doc.css('a').first.attr('title')).to eq issue.title
     end
 
     it 'escapes the title attribute' do
@@ -66,7 +66,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
 
     it 'includes default classes' do
       doc = reference_filter("Issue #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
     end
 
     it 'includes a data-project attribute' do
@@ -85,6 +85,20 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
       expect(link.attr('data-issue')).to eq issue.id.to_s
     end
 
+    it 'includes a data-original attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-original')
+      expect(link.attr('data-original')).to eq reference
+    end
+
+    it 'does not escape the data-original attribute' do
+      inner_html = 'element <code>node</code> inside'
+      doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>})
+      expect(doc.children.first.attr('data-original')).to eq inner_html
+    end
+
     it 'supports an :only_path context' do
       doc = reference_filter("Issue #{reference}", only_path: true)
       link = doc.css('a').first.attr('href')
@@ -103,6 +117,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
   end
 
   context 'cross-project reference' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:namespace) { create(:namespace, name: 'cross-reference') }
     let(:project2)  { create(:empty_project, :public, namespace: namespace) }
     let(:issue)     { create(:issue, project: project2) }
@@ -143,6 +159,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
   end
 
   context 'cross-project URL reference' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:namespace) { create(:namespace, name: 'cross-reference') }
     let(:project2)  { create(:empty_project, :public, namespace: namespace) }
     let(:issue)     { create(:issue, project: project2) }
@@ -162,56 +180,49 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
   end
 
   context 'cross-project reference in link href' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:namespace) { create(:namespace, name: 'cross-reference') }
     let(:project2)  { create(:empty_project, :public, namespace: namespace) }
     let(:issue)     { create(:issue, project: project2) }
-    let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
+    let(:reference) { issue.to_reference(project) }
+    let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
 
     it 'links to a valid reference' do
-      doc = reference_filter("See #{reference}")
+      doc = reference_filter("See #{reference_link}")
 
       expect(doc.css('a').first.attr('href')).
         to eq helper.url_for_issue(issue.iid, project2)
     end
 
     it 'links with adjacent text' do
-      doc = reference_filter("Fixed (#{reference}.)")
+      doc = reference_filter("Fixed (#{reference_link}.)")
       expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
     end
   end
 
   context 'cross-project URL in link href' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:namespace) { create(:namespace, name: 'cross-reference') }
     let(:project2)  { create(:empty_project, :public, namespace: namespace) }
     let(:issue)     { create(:issue, project: project2) }
-    let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+    let(:reference) { "#{helper.url_for_issue(issue.iid, project2) + "#note_123"}" }
+    let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
 
     it 'links to a valid reference' do
-      doc = reference_filter("See #{reference}")
+      doc = reference_filter("See #{reference_link}")
 
       expect(doc.css('a').first.attr('href')).
         to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
     end
 
     it 'links with adjacent text' do
-      doc = reference_filter("Fixed (#{reference}.)")
+      doc = reference_filter("Fixed (#{reference_link}.)")
       expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
     end
   end
 
-  context 'referencing external issues' do
-    let(:project) { create(:redmine_project) }
-
-    it 'renders internal issue IDs as external issue links' do
-      doc = reference_filter('#1')
-      link = doc.css('a').first
-
-      expect(link.attr('data-reference-type')).to eq('external_issue')
-      expect(link.attr('title')).to eq('Issue in Redmine')
-      expect(link.attr('data-external-issue')).to eq('1')
-    end
-  end
-
   describe '#issues_per_Project' do
     context 'using an internal issue tracker' do
       it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 9276a1540070e207feaecbe8b7126fa4d4fe70d5..9c09f00ae8aa72793d30990aa5478c8b2ac9701b 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -21,7 +21,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
 
   it 'includes default classes' do
     doc = reference_filter("Label #{reference}")
-    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip'
   end
 
   it 'includes a data-project attribute' do
@@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
     end
   end
 
+  describe 'group label references' do
+    let(:group)       { create(:group) }
+    let(:project)     { create(:empty_project, :public, namespace: group) }
+    let(:group_label) { create(:group_label, name: 'gfm references', group: group) }
+
+    context 'without project reference' do
+      let(:reference) { group_label.to_reference(format: :name) }
+
+      it 'links to a valid reference' do
+        doc = reference_filter("See #{reference}", project: project)
+
+        expect(doc.css('a').first.attr('href')).to eq urls.
+          namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+        expect(doc.text).to eq 'See gfm references'
+      end
+
+      it 'links with adjacent text' do
+        doc = reference_filter("Label (#{reference}.)")
+        expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
+      end
+
+      it 'ignores invalid label names' do
+        exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}")
+
+        expect(reference_filter(act).to_html).to eq exp
+      end
+    end
+
+    context 'with project reference' do
+      let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
+
+      it 'links to a valid reference' do
+        doc = reference_filter("See #{reference}", project: project)
+
+        expect(doc.css('a').first.attr('href')).to eq urls.
+          namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+        expect(doc.text).to eq 'See gfm references'
+      end
+
+      it 'links with adjacent text' do
+        doc = reference_filter("Label (#{reference}.)")
+        expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
+      end
+
+      it 'ignores invalid label names' do
+        exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
+
+        expect(reference_filter(act).to_html).to eq exp
+      end
+    end
+  end
+
   describe 'cross project label references' do
     context 'valid project referenced' do
       let(:another_project)  { create(:empty_project, :public) }
@@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
       end
     end
   end
+
+  describe 'cross group label references' do
+    context 'valid project referenced' do
+      let(:group) { create(:group) }
+      let(:project) { create(:empty_project, :public, namespace: group) }
+      let(:another_group) { create(:group) }
+      let(:another_project)  { create(:empty_project, :public, namespace: another_group) }
+      let(:project_name) { another_project.name_with_namespace }
+      let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
+      let(:reference) { another_project.to_reference + group_label.to_reference }
+
+      let!(:result) { reference_filter("See #{reference}", project: project) }
+
+      it 'points to referenced project issues page' do
+        expect(result.css('a').first.attr('href'))
+          .to eq urls.namespace_project_issues_url(another_project.namespace,
+                                                   another_project,
+                                                   label_name: group_label.name)
+      end
+
+      it 'has valid color' do
+        expect(result.css('a span').first.attr('style'))
+          .to match /background-color: #00ff00/
+      end
+
+      it 'contains cross project content' do
+        expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
+      end
+    end
+  end
 end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 805acf1c8b365bd7b65a56acd7a9283b715fb531..274258a045cf45abf906b35739e11135728b9224 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -46,7 +46,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
 
     it 'includes a title attribute' do
       doc = reference_filter("Merge #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
+      expect(doc.css('a').first.attr('title')).to eq merge.title
     end
 
     it 'escapes the title attribute' do
@@ -58,7 +58,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
 
     it 'includes default classes' do
       doc = reference_filter("Merge #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip'
     end
 
     it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 9424f2363e1c4db019334dc28b5547fb1037e8f8..7419863d848051b524b89e7ad3b23b94a11f6e90 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -20,7 +20,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
 
   it 'includes default classes' do
     doc = reference_filter("Milestone #{reference}")
-    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip'
   end
 
   it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index f181125156bf5a6e5994625ddefaffcf5b50caf3..0140a91c7bac23050ad57d3414eff76223501802 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do
         and_return(parser_class)
     end
 
-    it 'removes unpermitted Project references' do
-      user = create(:user)
-      project = create(:empty_project)
+    context 'valid projects' do
+      before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
 
-      link = reference_link(project: project.id, reference_type: 'test')
-      doc = filter(link, current_user: user)
+      it 'allows permitted Project references' do
+        user = create(:user)
+        project = create(:empty_project)
+        project.team << [user, :master]
+
+        link = reference_link(project: project.id, reference_type: 'test')
+        doc = filter(link, current_user: user)
 
-      expect(doc.css('a').length).to eq 0
+        expect(doc.css('a').length).to eq 1
+      end
     end
 
-    it 'allows permitted Project references' do
-      user = create(:user)
-      project = create(:empty_project)
-      project.team << [user, :master]
+    context 'invalid projects' do
+      before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
 
-      link = reference_link(project: project.id, reference_type: 'test')
-      doc = filter(link, current_user: user)
+      it 'removes unpermitted references' do
+        user = create(:user)
+        project = create(:empty_project)
 
-      expect(doc.css('a').length).to eq 1
-    end
+        link = reference_link(project: project.id, reference_type: 'test')
+        doc = filter(link, current_user: user)
 
-    it 'handles invalid Project references' do
-      link = reference_link(project: 12345, reference_type: 'test')
+        expect(doc.css('a').length).to eq 0
+      end
+
+      it 'handles invalid references' do
+        link = reference_link(project: 12345, reference_type: 'test')
 
-      expect { filter(link) }.not_to raise_error
+        expect { filter(link) }.not_to raise_error
+      end
     end
   end
 
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 6b58f3e43ee1153f91461873052cfcd1a235d10d..2bfa51deb2063406c69d8a1e4c6d45af1231c030 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -50,14 +50,6 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
     end
   end
 
-  shared_examples :relative_to_requested do
-    it 'rebuilds URL relative to the requested path' do
-      doc = filter(link('users.md'))
-      expect(doc.at_css('a')['href']).
-        to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
-    end
-  end
-
   context 'with a project_wiki' do
     let(:project_wiki) { double('ProjectWiki') }
     include_examples :preserve_unchanged
@@ -188,12 +180,38 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
 
     context 'when requested path is a file in the repo' do
       let(:requested_path) { 'doc/api/README.md' }
-      include_examples :relative_to_requested
+      it 'rebuilds URL relative to the containing directory' do
+        doc = filter(link('users.md'))
+        expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md"
+      end
     end
 
     context 'when requested path is a directory in the repo' do
-      let(:requested_path) { 'doc/api' }
-      include_examples :relative_to_requested
+      let(:requested_path) { 'doc/api/' }
+      it 'rebuilds URL relative to the directory' do
+        doc = filter(link('users.md'))
+        expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md"
+      end
+    end
+
+    context 'when ref name contains percent sign' do
+      let(:ref) { '100%branch' }
+      let(:commit) { project.commit('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') }
+      let(:requested_path) { 'foo/bar/' }
+      it 'correctly escapes the ref' do
+        doc = filter(link('.gitkeep'))
+        expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/foo/bar/.gitkeep"
+      end
+    end
+
+    context 'when requested path is a directory with space in the repo' do
+      let(:ref) { 'master' }
+      let(:commit) { project.commit('38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e') }
+      let(:requested_path) { 'with space/' }
+      it 'does not escape the space twice' do
+        doc = filter(link('README.md'))
+        expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/with%20space/README.md"
+      end
     end
   end
 
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 5068ddd7faa5c9f8537830c88617b8d043360e5d..9b92d1a392624f18ec7dfbcd10f59bc589a27a79 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -39,7 +39,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
 
     it 'includes a title attribute' do
       doc = reference_filter("Snippet #{reference}")
-      expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+      expect(doc.css('a').first.attr('title')).to eq snippet.title
     end
 
     it 'escapes the title attribute' do
@@ -51,7 +51,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
 
     it 'includes default classes' do
       doc = reference_filter("Snippet #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet has-tooltip'
     end
 
     it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index b1370bca8332dfe2fbe95fddb2a89c7e7a8fad2b..d265d29ee86990066e43a29b2c23ce551e854480 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"><code>def fun end</code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>def fun end</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"><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" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</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"><code>This is a test</code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>This is a test</code></pre>')
     end
   end
 
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
 
     it "highlights as plaintext" do
       result = filter('<pre><code class="ruby">This is a test</code></pre>')
-      expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>')
+      expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>')
     end
   end
 end
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
deleted file mode 100644
index 569cbc885c76e68b9968eac4a98c148427cdae10..0000000000000000000000000000000000000000
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::TaskListFilter, lib: true do
-  include FilterSpecHelper
-
-  it 'does not apply `task-list` class to non-task lists' do
-    exp = act = %(<ul><li>Item</li></ul>)
-    expect(filter(act).to_html).to eq exp
-  end
-
-  it 'applies `task-list` to single-item task lists' do
-    act = filter('<ul><li>[ ] Task 1</li></ul>')
-
-    expect(act.to_html).to start_with '<ul class="task-list">'
-  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 108b36a97cc8e81cf53a05b14ded6c752cdcddd6..5bfeb82e738c41e87787eccce894d06c8e2844b8 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -24,6 +24,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
   end
 
   context 'mentioning @all' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:reference) { User.reference_prefix + 'all' }
 
     before do
@@ -31,13 +33,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
     end
 
     it 'supports a special @all mention' do
+      project.team << [user, :developer]
       doc = reference_filter("Hey #{reference}", author: user)
+
       expect(doc.css('a').length).to eq 1
       expect(doc.css('a').first.attr('href'))
         .to eq urls.namespace_project_url(project.namespace, project)
     end
 
     it 'includes a data-author attribute when there is an author' do
+      project.team << [user, :developer]
       doc = reference_filter(reference, author: user)
 
       expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
@@ -48,9 +53,17 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
 
       expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
     end
+
+    it 'ignores reference to all when the user is not a project member' do
+      doc = reference_filter("Hey #{reference}", author: user)
+
+      expect(doc.css('a').length).to eq 0
+    end
   end
 
   context 'mentioning a user' do
+    it_behaves_like 'a reference containing an element node'
+
     it 'links to a User' do
       doc = reference_filter("Hey #{reference}")
       expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
@@ -80,6 +93,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
   end
 
   context 'mentioning a group' do
+    it_behaves_like 'a reference containing an element node'
+
     let(:group)     { create(:group) }
     let(:reference) { group.to_reference }
 
@@ -104,7 +119,7 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
 
   it 'includes default classes' do
     doc = reference_filter("Hey #{reference}")
-    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip'
   end
 
   it 'supports an :only_path context' do
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
index 98f76f36fd50349fe383e1142e7104912c265a1c..49556074278ae67931989f84dc48d1a8248ae1e2 100644
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do
         with(project, user,
              requested_path: 'foo',
              project_wiki: wiki,
-             ref: 'bar',
-             pipeline: :note).
+             ref: 'bar').
         and_call_original
 
       expect_any_instance_of(Banzai::ObjectRenderer).
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index bcdb95250caee162af0ffc151c907ffa832b6add..6bcda87c99971cd00bdce9c0a86b4da8f0f7bb6d 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do
   let(:project) { create(:empty_project) }
   let(:user) { project.owner }
 
+  def fake_object(attrs = {})
+    object = double(attrs.merge("new_record?" => true, "destroyed?" => true))
+    allow(object).to receive(:markdown_cache_field_for).with(:note).and_return(:note_html)
+    allow(object).to receive(:banzai_render_context).with(:note).and_return(project: nil, author: nil)
+    allow(object).to receive(:update_column).with(:note_html, anything).and_return(true)
+    object
+  end
+
   describe '#render' do
     it 'renders and redacts an Array of objects' do
       renderer = described_class.new(project, user)
-      object = double(:object, note: 'hello', note_html: nil)
+      object = fake_object(note: 'hello', note_html: nil)
 
       expect(renderer).to receive(:render_objects).with([object], :note).
         and_call_original
@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do
         with(an_instance_of(Array)).
         and_call_original
 
-      expect(object).to receive(:note_html=).with('<p>hello</p>')
+      expect(object).to receive(:redacted_note_html=).with('<p dir="auto">hello</p>')
       expect(object).to receive(:user_visible_reference_count=).with(0)
 
       renderer.render([object], :note)
@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do
 
   describe '#render_objects' do
     it 'renders an Array of objects' do
-      object = double(:object, note: 'hello')
+      object = fake_object(note: 'hello', note_html: nil)
 
       renderer = described_class.new(project, user)
 
@@ -57,74 +65,50 @@ describe Banzai::ObjectRenderer do
   end
 
   describe '#context_for' do
-    let(:object) { double(:object, note: 'hello') }
+    let(:object) { fake_object(note: 'hello') }
     let(:renderer) { described_class.new(project, user) }
 
     it 'returns a Hash' do
       expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
     end
 
-    it 'includes the cache key' do
+    it 'includes the banzai render context for the object' do
+      expect(object).to receive(:banzai_render_context).with(:note).and_return(foo: :bar)
       context = renderer.context_for(object, :note)
-
-      expect(context[:cache_key]).to eq([object, :note])
-    end
-
-    context 'when the object responds to "author"' do
-      it 'includes the author in the context' do
-        expect(object).to receive(:author).and_return('Alice')
-
-        context = renderer.context_for(object, :note)
-
-        expect(context[:author]).to eq('Alice')
-      end
-    end
-
-    context 'when the object does not respond to "author"' do
-      it 'does not include the author in the context' do
-        context = renderer.context_for(object, :note)
-
-        expect(context.key?(:author)).to eq(false)
-      end
+      expect(context).to have_key(:foo)
+      expect(context[:foo]).to eq(:bar)
     end
   end
 
   describe '#render_attributes' do
     it 'renders the attribute of a list of objects' do
-      objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
-      renderer = described_class.new(project, user, pipeline: :note)
+      objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)]
+      renderer = described_class.new(project, user)
 
-      expect(Banzai).to receive(:cache_collection_render).
-        with([
-          { text: 'hello', context: renderer.context_for(objects[0], :note) },
-          { text: 'bye', context: renderer.context_for(objects[1], :note) }
-        ]).
-        and_call_original
+      objects.each do |object|
+        expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
+      end
 
       docs = renderer.render_attributes(objects, :note)
 
       expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
-      expect(docs[0].to_html).to eq('<p>hello</p>')
+      expect(docs[0].to_html).to eq('<p dir="auto">hello</p>')
 
       expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
-      expect(docs[1].to_html).to eq('<p>bye</p>')
+      expect(docs[1].to_html).to eq('<p dir="auto">bye</p>')
     end
 
     it 'returns when no objects to render' do
       objects = []
       renderer = described_class.new(project, user, pipeline: :note)
 
-      expect(Banzai).to receive(:cache_collection_render).
-        with([]).
-        and_call_original
-
       expect(renderer.render_attributes(objects, :note)).to eq([])
     end
   end
 
   describe '#base_context' do
     let(:context) do
-      described_class.new(project, user, pipeline: :note).base_context
+      described_class.new(project, user, foo: :bar).base_context
     end
 
     it 'returns a Hash' do
@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do
     end
 
     it 'includes the custom attributes' do
-      expect(context[:pipeline]).to eq(:note)
+      expect(context[:foo]).to eq(:bar)
     end
 
     it 'includes the current user' do
diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
index 76f4207181065fb78202c6845c3f88d580ffec93..8cce1b96698da7b3b81358fb8fe45a274ddc7169 100644
--- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
@@ -4,11 +4,11 @@ describe Banzai::Pipeline::DescriptionPipeline do
   def parse(html)
     # When we pass HTML to Redcarpet, it gets wrapped in `p` tags...
     # ...except when we pass it pre-wrapped text. Rabble rabble.
-    unwrap = !html.start_with?('<p>')
+    unwrap = !html.start_with?('<p ')
 
     output = described_class.to_html(html, project: spy)
 
-    output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap
+    output.gsub!(%r{\A<p dir="auto">(.*)</p>(.*)\z}, '\1\2') if unwrap
 
     output
   end
@@ -27,11 +27,17 @@ describe Banzai::Pipeline::DescriptionPipeline do
     end
   end
 
-  %w(b i strong em a ins del sup sub p).each do |elem|
+  %w(b i strong em a ins del sup sub).each do |elem|
     it "still allows '#{elem}' elements" do
       exp = act = "<#{elem}>Description</#{elem}>"
 
       expect(parse(act).strip).to eq exp
     end
   end
+
+  it "still allows 'p' elements" do
+    exp = act = "<p dir=\"auto\">Description</p>"
+
+    expect(parse(act).strip).to eq exp
+  end
 end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2501b638774789070bb9eee3805c678b4e43ee19
--- /dev/null
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::FullPipeline do
+  describe 'References' do
+    let(:project) { create(:empty_project, :public) }
+    let(:issue)   { create(:issue, project: project) }
+
+    it 'handles markdown inside a reference' do
+      markdown = "[some `code` inside](#{issue.to_reference})"
+      result = described_class.call(markdown, project: project)
+      link_content = result[:output].css('a').inner_html
+      expect(link_content).to eq('some <code>code</code> inside')
+    end
+
+    it 'sanitizes reference HTML' do
+      link_label = '<script>bad things</script>'
+      markdown = "[#{link_label}](#{issue.to_reference})"
+      result = described_class.to_html(markdown, project: project)
+      expect(result).not_to include(link_label)
+    end
+
+    it 'escapes the data-original attribute on a reference' do
+      markdown = %Q{[">bad things](#{issue.to_reference})}
+      result = described_class.to_html(markdown, project: project)
+      expect(result).to include(%{data-original='\"&gt;bad things'})
+    end
+  end
+end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 51c89ac4889f0adf0bf3bc078de60299d9b3f510..ac9bde6baf16e9c0a9bee91680974963d7f696c6 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -127,6 +127,13 @@ describe Banzai::Pipeline::WikiPipeline do
 
               expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
             end
+
+            it 'rewrites links with anchor' do
+              markdown = '[Link to Header](start-page#title)'
+              output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+              expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"")
+            end
           end
 
           describe "when creating root links" do
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 254657a881da3a40a038dbca0adfe06e85e44c9b..6d2c141e18b2bde4978b731bc88aaa598042a408 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -6,39 +6,60 @@ describe Banzai::Redactor do
   let(:redactor) { described_class.new(project, user) }
 
   describe '#redact' do
-    it 'redacts an Array of documents' do
-      doc1 = Nokogiri::HTML.
-        fragment('<a class="gfm" data-reference-type="issue">foo</a>')
-
-      doc2 = Nokogiri::HTML.
-        fragment('<a class="gfm" data-reference-type="issue">bar</a>')
-
-      expect(redactor).to receive(:nodes_visible_to_user).and_return([])
-
-      redacted_data = redactor.redact([doc1, doc2])
-
-      expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
-      expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
-      expect(doc1.to_html).to eq('foo')
-      expect(doc2.to_html).to eq('bar')
+    context 'when reference not visible to user' do
+      before do
+        expect(redactor).to receive(:nodes_visible_to_user).and_return([])
+      end
+
+      it 'redacts an array of documents' do
+        doc1 = Nokogiri::HTML.
+               fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+
+        doc2 = Nokogiri::HTML.
+               fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+
+        redacted_data = redactor.redact([doc1, doc2])
+
+        expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+        expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
+        expect(doc1.to_html).to eq('foo')
+        expect(doc2.to_html).to eq('bar')
+      end
+
+      it 'replaces redacted reference with inner HTML' do
+        doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+        redactor.redact([doc])
+        expect(doc.to_html).to eq('foo')
+      end
+
+      context 'when data-original attribute provided' do
+        let(:original_content) { '<code>foo</code>' }
+        it 'replaces redacted reference with original content' do
+          doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+          redactor.redact([doc])
+          expect(doc.to_html).to eq(original_content)
+        end
+      end
     end
 
-    it 'does not redact an Array of documents' do
-      doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
-      doc1 = Nokogiri::HTML.fragment(doc1_html)
+    context 'when reference visible to user' do
+      it 'does not redact an array of documents' do
+        doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
+        doc1 = Nokogiri::HTML.fragment(doc1_html)
 
-      doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
-      doc2 = Nokogiri::HTML.fragment(doc2_html)
+        doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
+        doc2 = Nokogiri::HTML.fragment(doc2_html)
 
-      nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
-      expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
+        nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
+        expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
 
-      redacted_data = redactor.redact([doc1, doc2])
+        redacted_data = redactor.redact([doc1, doc2])
 
-      expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
-      expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
-      expect(doc1.to_html).to eq(doc1_html)
-      expect(doc2.to_html).to eq(doc2_html)
+        expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+        expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
+        expect(doc1.to_html).to eq(doc1_html)
+        expect(doc2.to_html).to eq(doc2_html)
+      end
     end
   end
 
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index ac9c66e2663bb9f07d5de7d40af132850588848a..aa127f0179dd4d0bedd317b01b763bf920967ea1 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
     let(:link) { empty_html_link }
 
     context 'when the link has a data-project attribute' do
-      it 'returns the nodes if the attribute value equals the current project ID' do
+      it 'checks if user can read the resource' do
         link['data-project'] = project.id.to_s
 
-        expect(Ability.abilities).not_to receive(:allowed?)
-        expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
-      end
-
-      it 'returns the nodes if the user can read the project' do
-        other_project = create(:empty_project, :public)
-
-        link['data-project'] = other_project.id.to_s
-
-        expect(Ability.abilities).to receive(:allowed?).
-          with(user, :read_project, other_project).
-          and_return(true)
-
-        expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
-      end
-
-      it 'returns an empty Array when the attribute value is empty' do
-        link['data-project'] = ''
-
-        expect(subject.nodes_visible_to_user(user, [link])).to eq([])
-      end
-
-      it 'returns an empty Array when the user can not read the project' do
-        other_project = create(:empty_project, :public)
-
-        link['data-project'] = other_project.id.to_s
-
-        expect(Ability.abilities).to receive(:allowed?).
-          with(user, :read_project, other_project).
-          and_return(false)
+        expect(subject).to receive(:can_read_reference?).with(user, project)
 
-        expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+        subject.nodes_visible_to_user(user, [link])
       end
     end
 
@@ -221,7 +192,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
     it 'delegates the permissions check to the Ability class' do
       user = double(:user)
 
-      expect(Ability.abilities).to receive(:allowed?).
+      expect(Ability).to receive(:allowed?).
         with(user, :read_project, project)
 
       subject.can?(user, :read_project, project)
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 0b76d29fce019d36517fe96bb96355118011042f..412ffa77c36fa2b1a17de8c0484cf4e54f1b2cdb 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-commit'] = 123 }
+
+      it_behaves_like "referenced feature visibility", "repository"
+    end
+  end
+
   describe '#referenced_by' do
     context 'when the link has a data-project attribute' do
       before do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index ba982f38542745199074e5362eb0d3d8e8651492..96e55b0997ae12601f1e98d0e3874d57d0754f0b 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-commit-range'] = '123..456' }
+
+      it_behaves_like "referenced feature visibility", "repository"
+    end
+  end
+
   describe '#referenced_by' do
     context 'when the link has a data-project attribute' do
       before do
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index a6ef8394fe7a98dee317f1811c229bfe5651f142..50a5d1a19ba04198b2f0464555751f18d972d3a4 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-external-issue'] = 123 }
+
+      it_behaves_like "referenced feature visibility", "issues"
+    end
+  end
+
   describe '#referenced_by' do
     context 'when the link has a data-project attribute' do
       before do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 85cfe728b6a6a60806a7ecbd6b374da09205574d..6873b7b85f9efa7a09a117e23e0d7923147895e7 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -4,10 +4,10 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
   include ReferenceParserHelpers
 
   let(:project) { create(:empty_project, :public) }
-  let(:user) { create(:user) }
-  let(:issue) { create(:issue, project: project) }
-  subject { described_class.new(project, user) }
-  let(:link) { empty_html_link }
+  let(:user)    { create(:user) }
+  let(:issue)   { create(:issue, project: project) }
+  let(:link)    { empty_html_link }
+  subject       { described_class.new(project, user) }
 
   describe '#nodes_visible_to_user' do
     context 'when the link has a data-issue attribute' do
@@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
         link['data-issue'] = issue.id.to_s
       end
 
+      it_behaves_like "referenced feature visibility", "issues"
+
       it 'returns the nodes when the user can read the issue' do
         expect(Ability).to receive(:issues_readable_by_user).
           with([issue], user).
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index 77fda47f0e7c9f8e884d30e68187d37a2eb05b9d..8c540d35ddd077de8f5be1dd1bc65096fdef3d88 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-label'] = label.id.to_s }
+
+      it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+    end
+  end
+
   describe '#referenced_by' do
     describe 'when the link has a data-label attribute' do
       context 'using an existing label ID' do
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index cf89ad598ea5203a6bbf6f49e8885b5c0e017a10..cb69ca16800e5dd25f96a116ebc1299e807a1ffa 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
   subject { described_class.new(merge_request.target_project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      let(:project) { merge_request.target_project }
+
+      before do
+        project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+        link['data-merge-request'] = merge_request.id.to_s
+      end
+
+      it_behaves_like "referenced feature visibility", "merge_requests"
+    end
+  end
+
   describe '#referenced_by' do
     describe 'when the link has a data-merge-request attribute' do
       context 'using an existing merge request ID' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 6aa45a22cc48169841f6b3404c86a76cba1c071b..2d4d589ae345c0c02f68dbc503affc98738c0b9b 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-milestone'] = milestone.id.to_s }
+
+      it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+    end
+  end
+
   describe '#referenced_by' do
     describe 'when the link has a data-milestone attribute' do
       context 'using an existing milestone ID' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 59127b7c5d182e2f4dd3d463924f7cf2bda86ad3..d217a77580230b2ec3705766b826fa4c5bf25b75 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
   subject { described_class.new(project, user) }
   let(:link) { empty_html_link }
 
+  describe '#nodes_visible_to_user' do
+    context 'when the link has a data-issue attribute' do
+      before { link['data-snippet'] = snippet.id.to_s }
+
+      it_behaves_like "referenced feature visibility", "snippets"
+    end
+  end
+
   describe '#referenced_by' do
     describe 'when the link has a data-snippet attribute' do
       context 'using an existing snippet ID' do
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 9a82891297d3f2e10b3e6fd8379ff5f1ede383f2..fafc2cec5461ec13e9cba54bd037664e62579b7c 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
         end
 
         it 'returns the nodes if the user can read the group' do
-          expect(Ability.abilities).to receive(:allowed?).
+          expect(Ability).to receive(:allowed?).
             with(user, :read_group, group).
             and_return(true)
 
@@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
         end
 
         it 'returns an empty Array if the user can not read the group' do
-          expect(Ability.abilities).to receive(:allowed?).
+          expect(Ability).to receive(:allowed?).
             with(user, :read_group, group).
             and_return(false)
 
@@ -103,7 +103,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
           it 'returns the nodes if the attribute value equals the current project ID' do
             link['data-project'] = project.id.to_s
 
-            expect(Ability.abilities).not_to receive(:allowed?)
+            # Ensure that we dont call for Ability.allowed?
+            # When project_id in the node is equal to current project ID
+            expect(Ability).not_to receive(:allowed?)
 
             expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
           end
@@ -113,7 +115,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
 
             link['data-project'] = other_project.id.to_s
 
-            expect(Ability.abilities).to receive(:allowed?).
+            expect(Ability).to receive(:allowed?).
               with(user, :read_project, other_project).
               and_return(true)
 
@@ -125,7 +127,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
 
             link['data-project'] = other_project.id.to_s
 
-            expect(Ability.abilities).to receive(:allowed?).
+            expect(Ability).to receive(:allowed?).
               with(user, :read_project, other_project).
               and_return(false)
 
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aaa6b12e67ea67d6470a402cb547aab20e97743e
--- /dev/null
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Banzai::Renderer do
+  def expect_render(project = :project)
+    expected_context = { project: project }
+    expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context)
+  end
+
+  def expect_cache_update
+    expect(object).to receive(:update_column).with("field_html", :html)
+  end
+
+  def fake_object(*features)
+    markdown = :markdown if features.include?(:markdown)
+    html = :html if features.include?(:html)
+
+    object = double(
+      "object",
+      banzai_render_context: { project: :project },
+      field: markdown,
+      field_html: html
+    )
+
+    allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html")
+    allow(object).to receive(:new_record?).and_return(features.include?(:new))
+    allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed))
+
+    object
+  end
+
+  describe "#render_field" do
+    let(:renderer) { Banzai::Renderer }
+    let(:subject) { renderer.render_field(object, :field) }
+
+    context "with an empty cache" do
+      let(:object) { fake_object(:markdown) }
+      it "caches and returns the result" do
+        expect_render
+        expect_cache_update
+        expect(subject).to eq(:html)
+      end
+    end
+
+    context "with a filled cache" do
+      let(:object) { fake_object(:markdown, :html) }
+
+      it "uses the cache" do
+        expect_render.never
+        expect_cache_update.never
+        should eq(:html)
+      end
+    end
+
+    context "new object" do
+      let(:object) { fake_object(:new, :markdown) }
+
+      it "doesn't cache the result" do
+        expect_render
+        expect_cache_update.never
+        expect(subject).to eq(:html)
+      end
+    end
+
+    context "destroyed object" do
+      let(:object) { fake_object(:destroyed, :markdown) }
+
+      it "doesn't cache the result" do
+        expect_render
+        expect_cache_update.never
+        expect(subject).to eq(:html)
+      end
+    end
+  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 be51d942af7c140d00f5e5459e7fece59469b04c..84f21631719b9e7b8f581149b8ea97c1616dda49 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -754,6 +754,20 @@ module Ci
         it 'does return production' do
           expect(builds.size).to eq(1)
           expect(builds.first[:environment]).to eq(environment)
+          expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
+        end
+      end
+
+      context 'when hash is specified' do
+        let(:environment) do
+          { name: 'production',
+            url: 'http://production.gitlab.com' }
+        end
+
+        it 'does return production and URL' do
+          expect(builds.size).to eq(1)
+          expect(builds.first[:environment]).to eq(environment[:name])
+          expect(builds.first[:options]).to include(environment: environment)
         end
       end
 
@@ -770,15 +784,62 @@ module Ci
         let(:environment) { 1 }
 
         it 'raises error' do
-          expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+          expect { builds }.to raise_error(
+            'jobs:deploy_to_production:environment config should be a hash or a string')
         end
       end
 
       context 'is not a valid string' do
-        let(:environment) { 'production staging' }
+        let(:environment) { 'production:staging' }
 
         it 'raises error' do
-          expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+          expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
+        end
+      end
+
+      context 'when on_stop is specified' do
+        let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
+        let(:config) { { review: review, close_review: close_review }.compact }
+
+        context 'with matching job' do
+          let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
+
+          it 'does return a list of builds' do
+            expect(builds.size).to eq(2)
+            expect(builds.first[:environment]).to eq('review')
+          end
+        end
+
+        context 'without matching job' do
+          let(:close_review) { nil  }
+
+          it 'raises error' do
+            expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
+          end
+        end
+
+        context 'with close job without environment' do
+          let(:close_review) { { stage: 'deploy', script: 'test' } }
+
+          it 'raises error' do
+            expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
+          end
+        end
+
+        context 'with close job for different environment' do
+          let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
+
+          it 'raises error' do
+            expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
+          end
+        end
+
+        context 'with close job without stop action' do
+          let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
+
+          it 'raises error' do
+            expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
+          end
         end
       end
     end
@@ -1250,5 +1311,40 @@ EOT
         end
       end
     end
+
+    describe "#validation_message" do
+      context "when the YAML could not be parsed" do
+        it "returns an error about invalid configutaion" do
+          content = YAML.dump("invalid: yaml: test")
+
+          expect(GitlabCiYamlProcessor.validation_message(content))
+            .to eq "Invalid configuration format"
+        end
+      end
+
+      context "when the tags parameter is invalid" do
+        it "returns an error about invalid tags" do
+          content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+
+          expect(GitlabCiYamlProcessor.validation_message(content))
+            .to eq "jobs:rspec tags should be an array of strings"
+        end
+      end
+
+      context "when YAML content is empty" do
+        it "returns an error about missing content" do
+          expect(GitlabCiYamlProcessor.validation_message(''))
+            .to eq "Please provide content of .gitlab-ci.yml"
+        end
+      end
+
+      context "when the YAML is valid" do
+        it "does not return any errors" do
+          content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+
+          expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
+        end
+      end
+    end
   end
 end
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3101bed20fbab9193efd65e075ab69125342317a
--- /dev/null
+++ b/spec/lib/ci/mask_secret_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Ci::MaskSecret, lib: true do
+  subject { described_class }
+
+  describe '#mask' do
+    it 'masks exact number of characters' do
+      expect(mask('token', 'oke')).to eq('txxxn')
+    end
+
+    it 'masks multiple occurrences' do
+      expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
+    end
+
+    it 'does not mask if not found' do
+      expect(mask('token', 'not')).to eq('token')
+    end
+
+    it 'does support null token' do
+      expect(mask('token', nil)).to eq('token')
+    end
+
+    def mask(value, token)
+      subject.mask!(value.dup, token)
+    end
+  end
+end
diff --git a/spec/lib/constraints/constrainer_helper_spec.rb b/spec/lib/constraints/constrainer_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27c8d72aefc0386b0e8c4f5c48d21e6a80bd248b
--- /dev/null
+++ b/spec/lib/constraints/constrainer_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe ConstrainerHelper, lib: true do
+  include ConstrainerHelper
+
+  describe '#extract_resource_path' do
+    it { expect(extract_resource_path('/gitlab/')).to eq('gitlab') }
+    it { expect(extract_resource_path('///gitlab//')).to eq('gitlab') }
+    it { expect(extract_resource_path('/gitlab.atom')).to eq('gitlab') }
+
+    context 'relative url' do
+      before do
+        allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
+      end
+
+      it { expect(extract_resource_path('/gitlab/foo')).to eq('foo') }
+      it { expect(extract_resource_path('/foo/bar')).to eq('foo/bar') }
+    end
+  end
+end
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..42299b17c2bc471edb8146f4d53712fba2f1115b
--- /dev/null
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe GroupUrlConstrainer, lib: true do
+  let!(:group) { create(:group, path: 'gitlab') }
+
+  describe '#matches?' do
+    context 'root group' do
+      it { expect(subject.matches?(request '/gitlab')).to be_truthy }
+      it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
+      it { expect(subject.matches?(request '/gitlab/edit')).to be_falsey }
+      it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
+      it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
+    end
+  end
+
+  def request(path)
+    double(:request, path: path)
+  end
+end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b3f8530c609793d20123cf113ce5ecb3a457ec6c
--- /dev/null
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe UserUrlConstrainer, lib: true do
+  let!(:username) { create(:user, username: 'dz') }
+
+  describe '#matches?' do
+    it { expect(subject.matches?(request '/dz')).to be_truthy }
+    it { expect(subject.matches?(request '/dz.atom')).to be_truthy }
+    it { expect(subject.matches?(request '/dz/projects')).to be_falsey }
+    it { expect(subject.matches?(request '/gitlab')).to be_falsey }
+  end
+
+  def request(path)
+    double(:request, path: path)
+  end
+end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a6d8e6927e0db6b4f457388cba81c9795f3a0fba
--- /dev/null
+++ b/spec/lib/event_filter_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe EventFilter, lib: true do
+  describe '#apply_filter' do
+    let(:source_user) { create(:user) }
+    let!(:public_project) { create(:project, :public) }
+
+    let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
+    let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
+    let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
+    let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
+    let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+
+    it 'applies push filter' do
+      events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
+      expect(events).to contain_exactly(push_event)
+    end
+
+    it 'applies merged filter' do
+      events = EventFilter.new(EventFilter.merged).apply_filter(Event.all)
+      expect(events).to contain_exactly(merged_event)
+    end
+
+    it 'applies comments filter' do
+      events = EventFilter.new(EventFilter.comments).apply_filter(Event.all)
+      expect(events).to contain_exactly(comments_event)
+    end
+
+    it 'applies team filter' do
+      events = EventFilter.new(EventFilter.team).apply_filter(Event.all)
+      expect(events).to contain_exactly(joined_event, left_event)
+    end
+
+    it 'applies all filter' do
+      events = EventFilter.new(EventFilter.all).apply_filter(Event.all)
+      expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+    end
+
+    it 'applies no filter' do
+      events = EventFilter.new(nil).apply_filter(Event.all)
+      expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+    end
+
+    it 'applies unknown filter' do
+      events = EventFilter.new('').apply_filter(Event.all)
+      expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+    end
+  end
+end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..90bc7dad3799e42e3d8149a13a42fd34b1526ad9
--- /dev/null
+++ b/spec/lib/expand_variables_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ExpandVariables do
+  describe '#expand' do
+    subject { described_class.expand(value, variables) }
+
+    tests = [
+      { value: 'key',
+        result: 'key',
+        variables: []
+      },
+      { value: 'key$variable',
+        result: 'key',
+        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',
+        result: 'review/feature/add-review-apps',
+        variables: [
+          { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
+        ]
+      },
+    ]
+
+    tests.each do |test|
+      context "#{test[:value]} resolves to #{test[:result]}" do
+        let(:value) { test[:value] }
+        let(:variables) { test[:variables] }
+
+        it { is_expected.to eq(test[:result]) }
+      end
+    end
+  end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 36c77206a3f935255113d977ddd8074ad7bc4acc..0e85e302f292700270c59bb4f8f39defffa877e7 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -6,6 +6,7 @@ describe ExtractsPath, lib: true do
   include Gitlab::Routing.url_helpers
 
   let(:project) { double('project') }
+  let(:request) { double('request') }
 
   before do
     @project = project
@@ -15,9 +16,10 @@ describe ExtractsPath, lib: true do
     allow(project).to receive(:repository).and_return(repo)
     allow(project).to receive(:path_with_namespace).
       and_return('gitlab/gitlab-ci')
+    allow(request).to receive(:format=)
   end
 
-  describe '#assign_ref' do
+  describe '#assign_ref_vars' do
     let(:ref) { sample_commit[:id] }
     let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
 
@@ -30,26 +32,104 @@ describe ExtractsPath, lib: true do
       expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
     end
 
-    context 'escaped slash character in ref' do
-      let(:ref) { 'improve%2Fawesome' }
+    context 'ref contains %20' do
+      let(:ref) { 'foo%20bar' }
+
+      it 'is not converted to a space in @id' do
+        @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
 
-      it 'has no escape sequences in @ref or @logs_path' do
         assign_ref_vars
 
-        expect(@ref).to eq('improve/awesome')
-        expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
+        expect(@id).to start_with('foo%20bar/')
       end
     end
 
-    context 'ref contains %20' do
-      let(:ref) { 'foo%20bar' }
+    context 'path contains space' do
+      let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
 
-      it 'is not converted to a space in @id' do
-        @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+      it 'is not converted to %20 in @path' do
+        assign_ref_vars
+
+        expect(@path).to eq(params[:path])
+      end
+    end
+
+    context 'subclass overrides get_id' do
+      it 'uses ref returned by get_id' do
+        allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
 
         assign_ref_vars
 
-        expect(@id).to start_with('foo%20bar/')
+        expect(@id).to eq(get_id)
+      end
+    end
+
+    context 'ref only exists without .atom suffix' do
+      context 'with a path' do
+        let(:params) { { ref: 'v1.0.0.atom', path: 'README.md' } }
+
+        it 'renders a 404' do
+          expect(self).to receive(:render_404)
+
+          assign_ref_vars
+        end
+      end
+
+      context 'without a path' do
+        let(:params) { { ref: 'v1.0.0.atom' } }
+        before { assign_ref_vars }
+
+        it 'sets the un-suffixed version as @ref' do
+          expect(@ref).to eq('v1.0.0')
+        end
+
+        it 'sets the request format to Atom' do
+          expect(request).to have_received(:format=).with(:atom)
+        end
+      end
+    end
+
+    context 'ref exists with .atom suffix' do
+      context 'with a path' do
+        let(:params) { { ref: 'master.atom', path: 'README.md' } }
+
+        before do
+          repository = @project.repository
+          allow(repository).to receive(:commit).and_call_original
+          allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+
+          assign_ref_vars
+        end
+
+        it 'sets the suffixed version as @ref' do
+          expect(@ref).to eq('master.atom')
+        end
+
+        it 'does not change the request format' do
+          expect(request).not_to have_received(:format=)
+        end
+      end
+
+      context 'without a path' do
+        let(:params) { { ref: 'master.atom' } }
+
+        before do
+          repository = @project.repository
+          allow(repository).to receive(:commit).and_call_original
+          allow(repository).to receive(:commit).with('master.atom').and_return(repository.commit('master'))
+        end
+
+        it 'sets the suffixed version as @ref' do
+          assign_ref_vars
+
+          expect(@ref).to eq('master.atom')
+        end
+
+        it 'does not change the request format' do
+          expect(request).not_to receive(:format=)
+
+          assign_ref_vars
+        end
       end
     end
   end
@@ -106,4 +186,18 @@ describe ExtractsPath, lib: true do
       end
     end
   end
+
+  describe '#extract_ref_without_atom' do
+    it 'ignores any matching refs suffixed with atom' do
+      expect(extract_ref_without_atom('master.atom')).to eq('master')
+    end
+
+    it 'returns the longest matching ref' do
+      expect(extract_ref_without_atom('release/app/v1.0.0.atom')).to eq('release/app/v1.0.0')
+    end
+
+    it 'returns nil if there are no matching refs' do
+      expect(extract_ref_without_atom('foo.atom')).to eq(nil)
+    end
+  end
 end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b0772cad3123cf6ec9d78e34526cdbc263ce4570..c9d64e99f88f2d8756d9765c0241dd3e02b2af43 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -4,14 +4,53 @@ describe Gitlab::Auth, lib: true do
   let(:gl_auth) { described_class }
 
   describe 'find_for_git_client' do
-    it 'recognizes CI' do
-      token = '123'
+    context 'build token' do
+      subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
+
+      context 'for running build' do
+        let!(:build) { create(:ci_build, :running) }
+        let(:project) { build.project }
+
+        before do
+          expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token')
+        end
+
+        it 'recognises user-less build' do
+          expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
+        end
+
+        it 'recognises user token' do
+          build.update(user: create(:user))
+
+          expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
+        end
+      end
+
+      (HasStatus::AVAILABLE_STATUSES - ['running']).each do |build_status|
+        context "for #{build_status} build" do
+          let!(:build) { create(:ci_build, status: build_status) }
+          let(:project) { build.project }
+
+          before do
+            expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token')
+          end
+
+          it 'denies authentication' do
+            expect(subject).to eq(Gitlab::Auth::Result.new)
+          end
+        end
+      end
+    end
+
+    it 'recognizes other ci services' do
       project = create(:empty_project)
-      project.update_attributes(runners_token: token, builds_enabled: true)
+      project.create_drone_ci_service(active: true)
+      project.drone_ci_service.update(token: 'token')
+
       ip = 'ip'
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
-      expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
+      expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
     end
 
     it 'recognizes master passwords' do
@@ -19,7 +58,25 @@ describe Gitlab::Auth, lib: true do
       ip = 'ip'
 
       expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
-      expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+      expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+    end
+
+    it 'recognizes user lfs tokens' do
+      user = create(:user)
+      ip = 'ip'
+      token = Gitlab::LfsToken.new(user).token
+
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+      expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+    end
+
+    it 'recognizes deploy key lfs tokens' do
+      key = create(:deploy_key)
+      ip = 'ip'
+      token = Gitlab::LfsToken.new(key).token
+
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
+      expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
     end
 
     it 'recognizes OAuth tokens' do
@@ -29,7 +86,7 @@ describe Gitlab::Auth, lib: true do
       ip = 'ip'
 
       expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
-      expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+      expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
     end
 
     it 'returns double nil for invalid credentials' do
@@ -91,4 +148,30 @@ describe Gitlab::Auth, lib: true do
       end
     end
   end
+
+  private
+
+  def build_authentication_abilities
+    [
+      :read_project,
+      :build_download_code,
+      :build_read_container_image,
+      :build_create_container_image
+    ]
+  end
+
+  def read_authentication_abilities
+    [
+      :read_project,
+      :download_code,
+      :read_container_image
+    ]
+  end
+
+  def full_authentication_abilities
+    read_authentication_abilities + [
+      :push_code,
+      :create_container_image
+    ]
+  end
 end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 6e5ba21138210ba34d9212c30d6fc5bae044229e..4b08a02ec730f948d0cb71f745c18a827857c26f 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -1,4 +1,5 @@
 require 'spec_helper'
+require 'stringio'
 
 describe Gitlab::Shell, lib: true do
   let(:project) { double('Project', id: 7, path: 'diaspora') }
@@ -13,7 +14,6 @@ describe Gitlab::Shell, lib: true do
   it { is_expected.to respond_to :add_repository }
   it { is_expected.to respond_to :remove_repository }
   it { is_expected.to respond_to :fork_repository }
-  it { is_expected.to respond_to :gc }
   it { is_expected.to respond_to :add_namespace }
   it { is_expected.to respond_to :rm_namespace }
   it { is_expected.to respond_to :mv_namespace }
@@ -21,15 +21,15 @@ describe Gitlab::Shell, lib: true do
 
   it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
 
-  describe 'generate_and_link_secret_token' do
+  describe 'memoized secret_token' do
     let(:secret_file) { 'tmp/tests/.secret_shell_test' }
     let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
 
     before do
-      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
       allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
+      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
       FileUtils.mkdir('tmp/tests/shell-secret-test')
-      gitlab_shell.generate_and_link_secret_token
+      Gitlab::Shell.ensure_secret_token!
     end
 
     after do
@@ -38,21 +38,47 @@ describe Gitlab::Shell, lib: true do
     end
 
     it 'creates and links the secret token file' do
+      secret_token = Gitlab::Shell.secret_token
+
       expect(File.exist?(secret_file)).to be(true)
+      expect(File.read(secret_file).chomp).to eq(secret_token)
       expect(File.symlink?(link_file)).to be(true)
       expect(File.readlink(link_file)).to eq(secret_file)
     end
   end
 
+  describe '#add_key' do
+    it 'removes trailing garbage' do
+      allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
+      expect(Gitlab::Utils).to receive(:system_silent).with(
+        [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
+      )
+
+      gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+    end
+  end
+
   describe Gitlab::Shell::KeyAdder, lib: true do
     describe '#add_key' do
-      it 'normalizes space characters in the key' do
-        io = spy
+      it 'removes trailing garbage' do
+        io = spy(:io)
         adder = described_class.new(io)
 
-        adder.add_key('key-42', "sha-rsa foo\tbar\tbaz")
+        adder.add_key('key-42', "ssh-rsa foo bar\tbaz")
+
+        expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
+      end
+
+      it 'raises an exception if the key contains a tab' do
+        expect do
+          described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
+        end.to raise_error(Gitlab::Shell::Error)
+      end
 
-        expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz")
+      it 'raises an exception if the key contains a newline' do
+        expect do
+          described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned")
+        end.to raise_error(Gitlab::Shell::Error)
       end
     end
   end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index 1ff496024864e8b476d135ca9a6c25bd90e2c7a7..1547bd3228c0c39c170032814077f180bbdf6967 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do
     end
   end
 
-  context 'pipeline exists' do
-    let!(:pipeline) do
-      create(:ci_pipeline, project: project,
-                           sha: project.commit.id,
-                           ref: 'master')
-    end
+  context 'when latest successful pipeline exists' do
+    before do
+      create_pipeline do |pipeline|
+        create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
+        create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+      end
 
-    context 'builds exist' do
-      before do
-        create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40)
-        create(:ci_build, pipeline: pipeline, coverage: 60)
+      create_pipeline do |pipeline|
+        create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
       end
+    end
 
-      context 'particular job specified' do
-        let(:job_name) { 'first' }
+    context 'when particular job specified' do
+      let(:job_name) { 'first' }
 
-        it 'returns coverage for the particular job' do
-          expect(badge.status).to eq 40
-        end
+      it 'returns coverage for the particular job' do
+        expect(badge.status).to eq 40
       end
+    end
 
-      context 'particular job not specified' do
-        let(:job_name) { '' }
+    context 'when particular job not specified' do
+      let(:job_name) { '' }
+
+      it 'returns arithemetic mean for the pipeline' do
+        expect(badge.status).to eq 50
+      end
+    end
+  end
 
-        it 'returns arithemetic mean for the pipeline' do
-          expect(badge.status).to eq 50
-        end
+  context 'when only failed pipeline exists' do
+    before do
+      create_pipeline do |pipeline|
+        create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
       end
     end
 
-    context 'builds do not exist' do
-      it_behaves_like 'unknown coverage report'
+    it_behaves_like 'unknown coverage report'
 
-      context 'particular job specified' do
-        let(:job_name) { 'nonexistent' }
+    context 'particular job specified' do
+      let(:job_name) { 'nonexistent' }
 
-        it 'retruns nil' do
-          expect(badge.status).to be_nil
-        end
+      it 'retruns nil' do
+        expect(badge.status).to be_nil
       end
     end
   end
@@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do
   context 'pipeline does not exist' do
     it_behaves_like 'unknown coverage report'
   end
+
+  def create_pipeline
+    opts = { project: project, sha: project.commit.id, ref: 'master' }
+
+    create(:ci_pipeline, opts).tap do |pipeline|
+      yield pipeline
+      pipeline.update_status
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb
index 50f619ce26e6c1f96d79aa7f644828e93d4bd3ce..e251210949cf7cf572c2607eb3fb96bc125e35e7 100644
--- a/spec/lib/gitlab/ci/config/node/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do
   let(:entry) { described_class.new(config) }
 
   describe 'validations' do
-    before { entry.process! }
+    before { entry.compose! }
 
     context 'when entry config value is correct' do
       let(:config) do
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..df925ff1afd089b85ad25932f46b7608532cdd23
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb
@@ -0,0 +1,217 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Environment do
+  let(:entry) { described_class.new(config) }
+
+  before { entry.compose! }
+
+  context 'when configuration is a string' do
+    let(:config) { 'production' }
+
+    describe '#string?' do
+      it 'is string configuration' do
+        expect(entry).to be_string
+      end
+    end
+
+    describe '#hash?' do
+      it 'is not hash configuration' do
+        expect(entry).not_to be_hash
+      end
+    end
+
+    describe '#valid?' do
+      it 'is valid' do
+        expect(entry).to be_valid
+      end
+    end
+
+    describe '#value' do
+      it 'returns valid hash' do
+        expect(entry.value).to include(name: 'production')
+      end
+    end
+
+    describe '#name' do
+      it 'returns environment name' do
+        expect(entry.name).to eq 'production'
+      end
+    end
+
+    describe '#url' do
+      it 'returns environment url' do
+        expect(entry.url).to be_nil
+      end
+    end
+  end
+
+  context 'when configuration is a hash' do
+    let(:config) do
+      { name: 'development', url: 'https://example.gitlab.com' }
+    end
+
+    describe '#string?' do
+      it 'is not string configuration' do
+        expect(entry).not_to be_string
+      end
+    end
+
+    describe '#hash?' do
+      it 'is hash configuration' do
+        expect(entry).to be_hash
+      end
+    end
+
+    describe '#valid?' do
+      it 'is valid' do
+        expect(entry).to be_valid
+      end
+    end
+
+    describe '#value' do
+      it 'returns valid hash' do
+        expect(entry.value).to eq config
+      end
+    end
+
+    describe '#name' do
+      it 'returns environment name' do
+        expect(entry.name).to eq 'development'
+      end
+    end
+
+    describe '#url' do
+      it 'returns environment url' do
+        expect(entry.url).to eq 'https://example.gitlab.com'
+      end
+    end
+  end
+
+  context 'when valid action is used' do
+    let(:config) do
+      { name: 'production',
+        action: 'start' }
+    end
+
+    it 'is valid' do
+      expect(entry).to be_valid
+    end
+  end
+
+  context 'when invalid action is used' do
+    let(:config) do
+      { name: 'production',
+        action: 'invalid' }
+    end
+
+    describe '#valid?' do
+      it 'is not valid' do
+        expect(entry).not_to be_valid
+      end
+    end
+
+    describe '#errors' do
+      it 'contains error about invalid action' do
+        expect(entry.errors)
+          .to include 'environment action should be start or stop'
+      end
+    end
+  end
+
+  context 'when on_stop is used' do
+    let(:config) do
+      { name: 'production',
+        on_stop: 'close_app' }
+    end
+
+    it 'is valid' do
+      expect(entry).to be_valid
+    end
+  end
+
+  context 'when invalid on_stop is used' do
+    let(:config) do
+      { name: 'production',
+        on_stop: false }
+    end
+
+    describe '#valid?' do
+      it 'is not valid' do
+        expect(entry).not_to be_valid
+      end
+    end
+
+    describe '#errors' do
+      it 'contains error about invalid action' do
+        expect(entry.errors)
+          .to include 'environment on stop should be a string'
+      end
+    end
+  end
+
+  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' }
+    end
+
+    describe '#valid?' do
+      it 'is valid' do
+        expect(entry).to be_valid
+      end
+    end
+  end
+
+  context 'when configuration is invalid' do
+    context 'when configuration is an array' do
+      let(:config) { ['env'] }
+
+      describe '#valid?' do
+        it 'is not valid' do
+          expect(entry).not_to be_valid
+        end
+      end
+
+      describe '#errors' do
+        it 'contains error about invalid type' do
+          expect(entry.errors)
+            .to include 'environment config should be a hash or a string'
+        end
+      end
+    end
+
+    context 'when environment name is not present' do
+      let(:config) { { url: 'https://example.gitlab.com' } }
+
+      describe '#valid?' do
+        it 'is not valid' do
+          expect(entry).not_to be_valid
+        end
+      end
+
+      describe '#errors?' do
+        it 'contains error about missing environment name' do
+          expect(entry.errors)
+            .to include "environment name can't be blank"
+        end
+      end
+    end
+
+    context 'when invalid URL is used' do
+      let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
+
+      describe '#valid?' do
+        it 'is not valid' do
+          expect(entry).not_to be_valid
+        end
+      end
+
+      describe '#errors?' do
+        it 'contains error about invalid URL' do
+          expect(entry.errors)
+            .to include "environment url must be a valid url"
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
index d26185ba585c32a1606369cf60e3dee8228e23cc..a699089c56384add4c64d72f7c39d5259403f1db 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do
           .value(nil)
           .create!
 
-        expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+        expect(entry)
+          .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
       end
     end
 
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
index 2f87d270b36a4773fbbba4d47e2f6d2dd5236d19..12232ff7e2ff9384b7fa0a9da6747a97faf20f5a 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do
   end
 
   context 'when hash is valid' do
-    context 'when all entries defined' do
+    context 'when some entries defined' do
       let(:hash) do
         { before_script: ['ls', 'pwd'],
           image: 'ruby:2.2',
@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do
           stages: ['build', 'pages'],
           cache: { key: 'k', untracked: true, paths: ['public/'] },
           rspec: { script: %w[rspec ls] },
-          spinach: { script: 'spinach' } }
+          spinach: { before_script: [], variables: {}, script: 'spinach' } }
       end
 
-      describe '#process!' do
-        before { global.process! }
+      describe '#compose!' do
+        before { global.compose! }
 
         it 'creates nodes hash' do
           expect(global.descendants).to be_an Array
@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do
         end
       end
 
-      context 'when not processed' do
+      context 'when not composed' do
         describe '#before_script' do
           it 'returns nil' do
             expect(global.before_script).to be nil
@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do
         end
       end
 
-      context 'when processed' do
-        before { global.process! }
+      context 'when composed' do
+        before { global.compose! }
+
+        describe '#errors' do
+          it 'has no errors' do
+            expect(global.errors).to be_empty
+          end
+        end
 
         describe '#before_script' do
           it 'returns correct script' do
@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do
             expect(global.jobs).to eq(
               rspec: { name: :rspec,
                        script: %w[rspec ls],
-                       stage: 'test' },
+                       before_script: ['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' },
+                       after_script: ['make clean'] },
               spinach: { name: :spinach,
+                         before_script: [],
                          script: %w[spinach],
-                         stage: 'test' }
+                         commands: 'spinach',
+                         image: 'ruby:2.2',
+                         services: ['postgres:9.1', 'mysql:5.5'],
+                         stage: 'test',
+                         cache: { key: 'k', untracked: true, paths: ['public/'] },
+                         variables: {},
+                         after_script: ['make clean'] },
             )
           end
         end
@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do
     end
 
     context 'when most of entires not defined' do
-      let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
-      before { global.process! }
+      before { global.compose! }
+
+      let(:hash) do
+        { cache: { key: 'a' }, rspec: { script: %w[ls] } }
+      end
 
       describe '#nodes' do
         it 'instantizes all nodes' do
           expect(global.descendants.count).to eq 8
         end
 
-        it 'contains undefined nodes' do
+        it 'contains unspecified nodes' do
           expect(global.descendants.first)
-            .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+            .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
         end
       end
 
@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do
     # details.
     #
     context 'when entires specified but not defined' do
-      let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
-      before { global.process! }
+      before { global.compose! }
+
+      let(:hash) do
+        { variables: nil, rspec: { script: 'rspec' } }
+      end
 
       describe '#variables' do
         it 'undefined entry returns a default value' do
@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do
   end
 
   context 'when hash is not valid' do
-    before { global.process! }
+    before { global.compose! }
 
     let(:hash) do
       { before_script: 'ls' }
@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do
       expect(global.specified?).to be true
     end
   end
+
+  describe '#[]' do
+    before { global.compose! }
+
+    let(:hash) do
+      { cache: { key: 'a' }, rspec: { script: 'ls' } }
+    end
+
+    context 'when node exists' do
+      it 'returns correct entry' do
+        expect(global[:cache])
+          .to be_an_instance_of Gitlab::Ci::Config::Node::Cache
+        expect(global[:jobs][:rspec][:script].value).to eq ['ls']
+      end
+    end
+
+    context 'when node does not exist' do
+      it 'always return unspecified node' do
+        expect(global[:some][:unknown][:node])
+          .not_to be_specified
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
similarity index 66%
rename from spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
rename to spec/lib/gitlab/ci/config/node/hidden_spec.rb
index cc44e2cc05448e3c3b9e4b1da069e609c1fb8ec7..61e2a554419a361762b5755245b8f9d8e828e676 100644
--- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
@@ -1,15 +1,15 @@
 require 'spec_helper'
 
-describe Gitlab::Ci::Config::Node::HiddenJob do
+describe Gitlab::Ci::Config::Node::Hidden do
   let(:entry) { described_class.new(config) }
 
   describe 'validations' do
     context 'when entry config value is correct' do
-      let(:config) { { image: 'ruby:2.2' } }
+      let(:config) { [:some, :array] }
 
       describe '#value' do
         it 'returns key value' do
-          expect(entry.value).to eq(image: 'ruby:2.2')
+          expect(entry.value).to eq [:some, :array]
         end
       end
 
@@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do
     end
 
     context 'when entry value is not correct' do
-      context 'incorrect config value type' do
-        let(:config) { ['incorrect'] }
-
-        describe '#errors' do
-          it 'saves errors' do
-            expect(entry.errors)
-              .to include 'hidden job config should be a hash'
-          end
-        end
-      end
-
       context 'when config is empty' do
         let(:config) { {} }
 
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
index 1484fb60dd81eedc8e4a2416f44c119118381151..91f676dae03325136b71851e38205d9a66c167dc 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
 describe Gitlab::Ci::Config::Node::Job do
   let(:entry) { described_class.new(config, name: :rspec) }
 
-  before { entry.process! }
-
   describe 'validations' do
+    before { entry.compose! }
+
     context 'when entry config value is correct' do
       let(:config) { { script: 'rspec' } }
 
@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do
     end
   end
 
-  describe '#value' do
-    context 'when entry is correct' do
+  describe '#relevant?' do
+    it 'is a relevant entry' do
+      expect(entry).to be_relevant
+    end
+  end
+
+  describe '#compose!' do
+    let(:unspecified) { double('unspecified', 'specified?' => false) }
+
+    let(:specified) do
+      double('specified', 'specified?' => true, value: 'specified')
+    end
+
+    let(:deps) { double('deps', '[]' => unspecified) }
+
+    context 'when job config overrides global config' do
+      before { entry.compose!(deps) }
+
       let(:config) do
-        { before_script: %w[ls pwd],
-          script: 'rspec',
-          after_script: %w[cleanup] }
+        { image: 'some_image', cache: { key: 'test' } }
+      end
+
+      it 'overrides global config' do
+        expect(entry[:image].value).to eq 'some_image'
+        expect(entry[:cache].value).to eq(key: 'test')
+      end
+    end
+
+    context 'when job config does not override global config' do
+      before do
+        allow(deps).to receive('[]').with(:image).and_return(specified)
+        entry.compose!(deps)
       end
 
-      it 'returns correct value' do
-        expect(entry.value)
-          .to eq(name: :rspec,
-                 before_script: %w[ls pwd],
-                 script: %w[rspec],
-                 stage: 'test',
-                 after_script: %w[cleanup])
+      let(:config) { { script: 'ls', cache: { key: 'test' } } }
+
+      it 'uses config from global entry' do
+        expect(entry[:image].value).to eq 'specified'
+        expect(entry[:cache].value).to eq(key: 'test')
       end
     end
   end
 
-  describe '#relevant?' do
-    it 'is a relevant entry' do
-      expect(entry).to be_relevant
+  context 'when composed' do
+    before { entry.compose! }
+
+    describe '#value' do
+      before { entry.compose! }
+
+      context 'when entry is correct' do
+        let(:config) do
+          { before_script: %w[ls pwd],
+            script: 'rspec',
+            after_script: %w[cleanup] }
+        end
+
+        it 'returns correct value' do
+          expect(entry.value)
+            .to eq(name: :rspec,
+                   before_script: %w[ls pwd],
+                   script: %w[rspec],
+                   commands: "ls\npwd\nrspec",
+                   stage: 'test',
+                   after_script: %w[cleanup])
+        end
+      end
+    end
+
+    describe '#commands' do
+      let(:config) do
+        { before_script: %w[ls pwd], script: 'rspec' }
+      end
+
+      it 'returns a string of commands concatenated with new line character' do
+        expect(entry.commands).to eq "ls\npwd\nrspec"
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index b8d9c70479cc1a29e95deb05113565840c3876ba..929809339ef54f5bb922798f534135d7ad46ee13 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
   let(:entry) { described_class.new(config) }
 
   describe 'validations' do
-    before { entry.process! }
+    before { entry.compose! }
 
     context 'when entry config value is correct' do
       let(:config) { { rspec: { script: 'rspec' } } }
@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do
     end
   end
 
-  context 'when valid job entries processed' do
-    before { entry.process! }
+  context 'when valid job entries composed' do
+    before { entry.compose! }
 
     let(:config) do
       { rspec: { script: 'rspec' },
@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
         expect(entry.value).to eq(
           rspec: { name: :rspec,
                    script: %w[rspec],
+                   commands: 'rspec',
                    stage: 'test' },
           spinach: { name: :spinach,
                      script: %w[spinach],
+                     commands: 'spinach',
                      stage: 'test' })
       end
     end
@@ -74,7 +76,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
         expect(entry.descendants.first(2))
           .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
         expect(entry.descendants.last)
-          .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
+          .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
       end
     end
 
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
deleted file mode 100644
index 1ab5478dcfa01d2380c379391877f1d44030fa64..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/ci/config/node/null_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Null do
-  let(:null) { described_class.new(nil) }
-
-  describe '#leaf?' do
-    it 'is leaf node' do
-      expect(null).to be_leaf
-    end
-  end
-
-  describe '#valid?' do
-    it 'is always valid' do
-      expect(null).to be_valid
-    end
-  end
-
-  describe '#errors' do
-    it 'is does not contain errors' do
-      expect(null.errors).to be_empty
-    end
-  end
-
-  describe '#value' do
-    it 'returns nil' do
-      expect(null.value).to eq nil
-    end
-  end
-
-  describe '#relevant?' do
-    it 'is not relevant' do
-      expect(null.relevant?).to eq false
-    end
-  end
-
-  describe '#specified?' do
-    it 'is not defined' do
-      expect(null.specified?).to eq false
-    end
-  end
-end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
index ee7395362a96dc3978e4f77af87b7274a6fcc5d4..219a7e981d3b9fe22a5a1789caf0acca97cddd7f 100644
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -3,9 +3,7 @@ require 'spec_helper'
 describe Gitlab::Ci::Config::Node::Script do
   let(:entry) { described_class.new(config) }
 
-  describe '#process!' do
-    before { entry.process! }
-
+  describe 'validations' do
     context 'when entry config value is correct' do
       let(:config) { ['ls', 'pwd'] }
 
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
index 2d43e1c1a9d6477f421d9710b0f666efacdba62d..6bde86029631f9fa4e789213eef36c94f2c10e20 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -1,32 +1,41 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Config::Node::Undefined do
-  let(:undefined) { described_class.new(entry) }
-  let(:entry) { spy('Entry') }
+  let(:entry) { described_class.new }
+
+  describe '#leaf?' do
+    it 'is leaf node' do
+      expect(entry).to be_leaf
+    end
+  end
 
   describe '#valid?' do
-    it 'delegates method to entry' do
-      expect(undefined.valid).to eq entry
+    it 'is always valid' do
+      expect(entry).to be_valid
     end
   end
 
   describe '#errors' do
-    it 'delegates method to entry' do
-      expect(undefined.errors).to eq entry
+    it 'is does not contain errors' do
+      expect(entry.errors).to be_empty
     end
   end
 
   describe '#value' do
-    it 'delegates method to entry' do
-      expect(undefined.value).to eq entry
+    it 'returns nil' do
+      expect(entry.value).to eq nil
     end
   end
 
-  describe '#specified?' do
-    it 'is always false' do
-      allow(entry).to receive(:specified?).and_return(true)
+  describe '#relevant?' do
+    it 'is not relevant' do
+      expect(entry.relevant?).to eq false
+    end
+  end
 
-      expect(undefined.specified?).to be false
+  describe '#specified?' do
+    it 'is not defined' do
+      expect(entry.specified?).to eq false
     end
   end
 end
diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba3ceef24ceff0a3d4a73711e5d04c375ff7f142
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Unspecified do
+  let(:unspecified) { described_class.new(entry) }
+  let(:entry) { spy('Entry') }
+
+  describe '#valid?' do
+    it 'delegates method to entry' do
+      expect(unspecified.valid?).to eq entry
+    end
+  end
+
+  describe '#errors' do
+    it 'delegates method to entry' do
+      expect(unspecified.errors).to eq entry
+    end
+  end
+
+  describe '#value' do
+    it 'delegates method to entry' do
+      expect(unspecified.value).to eq entry
+    end
+  end
+
+  describe '#specified?' do
+    it 'is always false' do
+      allow(entry).to receive(:specified?).and_return(true)
+
+      expect(unspecified.specified?).to be false
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b26728a843cc7993f91eb2bac62f857db2b1ca83
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::PipelineDuration do
+  let(:calculated_duration) { calculate(data) }
+
+  shared_examples 'calculating duration' do
+    it do
+      expect(calculated_duration).to eq(duration)
+    end
+  end
+
+  context 'test sample A' do
+    let(:data) do
+      [[0, 1],
+       [1, 2],
+       [3, 4],
+       [5, 6]]
+    end
+
+    let(:duration) { 4 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample B' do
+    let(:data) do
+      [[0, 1],
+       [1, 2],
+       [2, 3],
+       [3, 4],
+       [0, 4]]
+    end
+
+    let(:duration) { 4 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample C' do
+    let(:data) do
+      [[0, 4],
+       [2, 6],
+       [5, 7],
+       [8, 9]]
+    end
+
+    let(:duration) { 8 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample D' do
+    let(:data) do
+      [[0, 1],
+       [2, 3],
+       [4, 5],
+       [6, 7]]
+    end
+
+    let(:duration) { 4 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample E' do
+    let(:data) do
+      [[0, 1],
+       [3, 9],
+       [3, 4],
+       [3, 5],
+       [3, 8],
+       [4, 5],
+       [4, 7],
+       [5, 8]]
+    end
+
+    let(:duration) { 7 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample F' do
+    let(:data) do
+      [[1, 3],
+       [2, 4],
+       [2, 4],
+       [2, 4],
+       [5, 8]]
+    end
+
+    let(:duration) { 6 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  context 'test sample G' do
+    let(:data) do
+      [[1, 3],
+       [2, 4],
+       [6, 7]]
+    end
+
+    let(:duration) { 4 }
+
+    it_behaves_like 'calculating duration'
+  end
+
+  def calculate(data)
+    periods = data.shuffle.map do |(first, last)|
+      Gitlab::Ci::PipelineDuration::Period.new(first, last)
+    end
+
+    Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
+  end
+end
diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f06d78694d60022d4da37695196260e2b2e5162f
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace_reader_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::TraceReader do
+  let(:path) { __FILE__ }
+  let(:lines) { File.readlines(path) }
+  let(:bytesize) { lines.sum(&:bytesize) }
+
+  it 'returns last few lines' do
+    10.times do
+      subject = build_subject
+      last_lines = random_lines
+
+      expected = lines.last(last_lines).join
+
+      expect(subject.read(last_lines: last_lines)).to eq(expected)
+    end
+  end
+
+  it 'returns everything if trying to get too many lines' do
+    expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join)
+  end
+
+  it 'raises an error if not passing an integer for last_lines' do
+    expect do
+      build_subject.read(last_lines: lines)
+    end.to raise_error(ArgumentError)
+  end
+
+  def random_lines
+    Random.rand(lines.size) + 1
+  end
+
+  def random_buffer
+    Random.rand(bytesize) + 1
+  end
+
+  def build_subject
+    described_class.new(__FILE__, buffer_size: random_buffer)
+  end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index de3f64249a25b29b63b01fbf95ecc6bff9b2c202..1bbaca0739af5d18d06dccc1e3b10443335e0b11 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
       context 'with an external issue tracker reference' do
         it 'extracts the referenced issue' do
           jira_project = create(:jira_project, name: 'JIRA_EXT1')
+          jira_project.team << [jira_project.creator, :master]
           jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
-          closing_issue_extractor = described_class.new jira_project
+          closing_issue_extractor = described_class.new(jira_project, jira_project.creator)
           message = "Resolve #{jira_issue.to_reference}"
 
           expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue])
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..39d892c18c033812e90e892973d6fb6d9f77bea3
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::FileCollection, lib: true do
+  let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
+  let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
+
+  describe '#files' do
+    it 'returns an array of Conflict::Files' do
+      expect(file_collection.files).to all(be_an_instance_of(Gitlab::Conflict::File))
+    end
+  end
+
+  describe '#default_commit_message' do
+    it 'matches the format of the git CLI commit message' do
+      expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
+Merge branch 'conflict-start' into 'conflict-resolvable'
+
+# Conflicts:
+#   files/ruby/popen.rb
+#   files/ruby/regex.rb
+EOM
+    end
+  end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..648d342ecf8e7d20f3c6e329c465f61968e37293
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::File, lib: true do
+  let(:project) { create(:project) }
+  let(:repository) { project.repository }
+  let(:rugged) { repository.rugged }
+  let(:their_commit) { rugged.branches['conflict-start'].target }
+  let(:our_commit) { rugged.branches['conflict-resolvable'].target }
+  let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
+  let(:index) { rugged.merge_commits(our_commit, their_commit) }
+  let(:conflict) { index.conflicts.last }
+  let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
+  let(:conflict_file) { Gitlab::Conflict::File.new(merge_file_result, conflict, merge_request: merge_request) }
+
+  describe '#resolve_lines' do
+    let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
+
+    context 'when resolving everything to the same side' do
+      let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h }
+      let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+      let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
+
+      it 'has the correct number of lines' do
+        expect(resolved_lines.length).to eq(expected_lines.length)
+      end
+
+      it 'has content matching the chosen lines' do
+        expect(resolved_lines.map(&:text)).to eq(expected_lines.map(&:text))
+      end
+    end
+
+    context 'with mixed resolutions' do
+      let(:resolution_hash) do
+        section_keys.map.with_index { |key, i| [key, i.even? ? 'head' : 'origin'] }.to_h
+      end
+
+      let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+
+      it 'has the correct number of lines' do
+        file_lines = conflict_file.lines.reject { |line| line.type == 'new' }
+
+        expect(resolved_lines.length).to eq(file_lines.length)
+      end
+
+      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'])
+      end
+    end
+
+    it 'raises MissingResolution when passed a hash without resolutions for all sections' do
+      empty_hash = section_keys.map { |key| [key, nil] }.to_h
+      invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
+
+      expect { conflict_file.resolve_lines({}) }.
+        to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+      expect { conflict_file.resolve_lines(empty_hash) }.
+        to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+      expect { conflict_file.resolve_lines(invalid_hash) }.
+        to raise_error(Gitlab::Conflict::File::MissingResolution)
+    end
+  end
+
+  describe '#highlight_lines!' do
+    def html_to_text(html)
+      CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n")
+    end
+
+    it 'modifies the existing lines' do
+      expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) }
+    end
+
+    it 'is called implicitly when rich_text is accessed on a line' do
+      expect(conflict_file).to receive(:highlight_lines!).once.and_call_original
+
+      conflict_file.lines.each(&:rich_text)
+    end
+
+    it 'sets the rich_text of the lines matching the text content' do
+      conflict_file.lines.each do |line|
+        expect(line.text).to eq(html_to_text(line.rich_text))
+      end
+    end
+  end
+
+  describe '#sections' do
+    it 'only inserts match lines when there is a gap between sections' do
+      conflict_file.sections.each_with_index do |section, i|
+        previous_line_number = 0
+        current_line_number = section[:lines].map(&:old_line).compact.min
+
+        if i > 0
+          previous_line_number = conflict_file.sections[i - 1][:lines].map(&:old_line).compact.last
+        end
+
+        if current_line_number == previous_line_number + 1
+          expect(section[:lines].first.type).not_to eq('match')
+        else
+          expect(section[:lines].first.type).to eq('match')
+          expect(section[:lines].first.text).to match(/\A@@ -#{current_line_number},\d+ \+\d+,\d+ @@ module Gitlab\Z/)
+        end
+      end
+    end
+
+    it 'sets conflict to false for sections with only unchanged lines' do
+      conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+        without_match = section[:lines].reject { |line| line.type == 'match' }
+
+        expect(without_match).to all(have_attributes(type: nil))
+      end
+    end
+
+    it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
+      conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+        without_match = section[:lines].reject { |line| line.type == 'match' }
+
+        expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
+      end
+    end
+
+    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'])
+        end
+      end
+    end
+
+    it 'adds unique IDs to conflict sections, and not to other sections' do
+      section_ids = []
+
+      conflict_file.sections.each do |section|
+        if section[:conflict]
+          expect(section).to have_key(:id)
+          section_ids << section[:id]
+        else
+          expect(section).not_to have_key(:id)
+        end
+      end
+
+      expect(section_ids.uniq).to eq(section_ids)
+    end
+
+    context 'with an example file' do
+      let(:file) do
+        <<FILE
+  # Ensure there is no match line header here
+  def username_regexp
+    default_regexp
+  end
+
+<<<<<<< files/ruby/regex.rb
+def project_name_regexp
+  /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+end
+
+def name_regexp
+  /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+def project_name_regex
+  %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+end
+
+def name_regex
+  %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+end
+
+# Some extra lines
+# To force a match line
+# To be created
+
+def path_regexp
+  default_regexp
+end
+
+<<<<<<< files/ruby/regex.rb
+def archive_formats_regexp
+  /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+def archive_formats_regex
+  %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+end
+
+def git_reference_regexp
+  # Valid git ref regexp, see:
+  # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+  %r{
+    (?!
+       (?# doesn't begins with)
+       \/|                    (?# rule #6)
+       (?# doesn't contain)
+       .*(?:
+          [\/.]\.|            (?# rule #1,3)
+          \/\/|               (?# rule #6)
+          @\{|                (?# rule #8)
+          \\                  (?# rule #9)
+       )
+    )
+    [^\000-\040\177~^:?*\[]+  (?# rule #4-5)
+    (?# doesn't end with)
+    (?<!\.lock)               (?# rule #1)
+    (?<![\/.])                (?# rule #6-7)
+  }x
+end
+
+protected
+
+<<<<<<< files/ruby/regex.rb
+def default_regexp
+  /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+def default_regex
+  %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+end
+FILE
+      end
+
+      let(:conflict_file) { Gitlab::Conflict::File.new({ data: file }, conflict, merge_request: merge_request) }
+      let(:sections) { conflict_file.sections }
+
+      it 'sets the correct match line headers' do
+        expect(sections[0][:lines].first).to have_attributes(type: 'match', text: '@@ -3,14 +3,14 @@')
+        expect(sections[3][:lines].first).to have_attributes(type: 'match', text: '@@ -19,26 +19,26 @@ def path_regexp')
+        expect(sections[6][:lines].first).to have_attributes(type: 'match', text: '@@ -47,52 +47,52 @@ end')
+      end
+
+      it 'does not add match lines where they are not needed' do
+        expect(sections[1][:lines].first.type).not_to eq('match')
+        expect(sections[2][:lines].first.type).not_to eq('match')
+        expect(sections[4][:lines].first.type).not_to eq('match')
+        expect(sections[5][:lines].first.type).not_to eq('match')
+        expect(sections[7][:lines].first.type).not_to eq('match')
+      end
+
+      it 'creates context sections of the correct length' do
+        expect(sections[0][:lines].reject(&:type).length).to eq(3)
+        expect(sections[2][:lines].reject(&:type).length).to eq(3)
+        expect(sections[3][:lines].reject(&:type).length).to eq(3)
+        expect(sections[5][:lines].reject(&:type).length).to eq(3)
+        expect(sections[6][:lines].reject(&:type).length).to eq(3)
+        expect(sections[8][:lines].reject(&:type).length).to eq(1)
+      end
+    end
+  end
+
+  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")
+    end
+
+    it 'includes the blob icon for the file' do
+      expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
+    end
+
+    context 'with the full_content option passed' do
+      it 'includes the full content of the conflict' do
+        expect(conflict_file.as_json(full_content: true)).to have_key(:content)
+      end
+
+      it 'includes the detected language of the conflict file' do
+        expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]).
+          to eq('ruby')
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16eb376635644762dcb29f047b5a3149161f33f1
--- /dev/null
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::Parser, lib: true do
+  let(:parser) { Gitlab::Conflict::Parser.new }
+
+  describe '#parse' do
+    def parse_text(text)
+      parser.parse(text, our_path: 'README.md', their_path: 'README.md')
+    end
+
+    context 'when the file has valid conflicts' do
+      let(:text) do
+        <<CONFLICT
+module Gitlab
+  module Regexp
+    extend self
+
+    def username_regexp
+      default_regexp
+    end
+
+<<<<<<< files/ruby/regex.rb
+    def project_name_regexp
+      /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+    end
+
+    def name_regexp
+      /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+    def project_name_regex
+      %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+    end
+
+    def name_regex
+      %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+    end
+
+    def path_regexp
+      default_regexp
+    end
+
+<<<<<<< files/ruby/regex.rb
+    def archive_formats_regexp
+      /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+    def archive_formats_regex
+      %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+    end
+
+    def git_reference_regexp
+      # Valid git ref regexp, see:
+      # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+      %r{
+        (?!
+           (?# doesn't begins with)
+           \/|                    (?# rule #6)
+           (?# doesn't contain)
+           .*(?:
+              [\/.]\.|            (?# rule #1,3)
+              \/\/|               (?# rule #6)
+              @\{|                (?# rule #8)
+              \\                  (?# rule #9)
+           )
+        )
+        [^\000-\040\177~^:?*\[]+  (?# rule #4-5)
+        (?# doesn't end with)
+        (?<!\.lock)               (?# rule #1)
+        (?<![\/.])                (?# rule #6-7)
+      }x
+    end
+
+    protected
+
+<<<<<<< files/ruby/regex.rb
+    def default_regexp
+      /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+    def default_regex
+      %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+    end
+  end
+end
+CONFLICT
+      end
+
+      let(:lines) do
+        parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
+      end
+
+      it 'sets our lines as new lines' do
+        expect(lines[8..13]).to all(have_attributes(type: 'new'))
+        expect(lines[26..27]).to all(have_attributes(type: 'new'))
+        expect(lines[56..57]).to all(have_attributes(type: 'new'))
+      end
+
+      it 'sets their lines as old lines' do
+        expect(lines[14..19]).to all(have_attributes(type: 'old'))
+        expect(lines[28..29]).to all(have_attributes(type: 'old'))
+        expect(lines[58..59]).to all(have_attributes(type: 'old'))
+      end
+
+      it 'sets non-conflicted lines as both' do
+        expect(lines[0..7]).to all(have_attributes(type: nil))
+        expect(lines[20..25]).to all(have_attributes(type: nil))
+        expect(lines[30..55]).to all(have_attributes(type: nil))
+        expect(lines[60..62]).to all(have_attributes(type: nil))
+      end
+
+      it 'sets consecutive line numbers for index, old_pos, and new_pos' do
+        old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
+        new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
+
+        expect(lines.map(&:index)).to eq(0.upto(62).to_a)
+        expect(old_line_numbers).to eq(1.upto(53).to_a)
+        expect(new_line_numbers).to eq(1.upto(53).to_a)
+      end
+    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
+      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"
+
+        expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+        expect { parse_text(start_text + start_text + end_text) }.
+          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+          not_to raise_error
+      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"
+
+        expect { parse_text(start_text + '=======' + end_text) }.
+          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+        expect { parse_text(start_text + start_text + end_text) }.
+          to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+        expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+          not_to raise_error
+      end
+
+      it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
+        start_text = "<<<<<<< README.md\n=======\n"
+
+        expect { parse_text(start_text) }.
+          to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+
+        expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
+          to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+      end
+    end
+
+    context 'other file types' do
+      it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
+        expect { parse_text('') }.
+          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+
+        expect { parse_text(nil) }.
+          to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+      end
+
+      it 'raises UnmergeableFile when the file is over 200 KB' do
+        expect { parse_text('a' * 204801) }.
+          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)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..01b2a55b63c43bfcf31270b944ee5e2cf56aa264
--- /dev/null
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::ContributionsCalendar do
+  let(:contributor) { create(:user) }
+  let(:user) { create(:user) }
+
+  let(:private_project) do
+    create(:empty_project, :private) do |project|
+      create(:project_member, user: contributor, project: project)
+    end
+  end
+
+  let(:public_project) do
+    create(:empty_project, :public) do |project|
+      create(:project_member, user: contributor, project: project)
+    end
+  end
+
+  let(:feature_project) do
+    create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+      create(:project_member, user: contributor, project: project).project
+    end
+  end
+
+  let(:today) { Time.now.to_date }
+  let(:last_week) { today - 7.days }
+  let(:last_year) { today - 1.year }
+
+  before do
+    travel_to today
+  end
+
+  after do
+    travel_back
+  end
+
+  def calendar(current_user = nil)
+    described_class.new(contributor, current_user)
+  end
+
+  def create_event(project, day)
+    @targets ||= {}
+    @targets[project] ||= create(:issue, project: project, author: contributor)
+
+    Event.create!(
+      project: project,
+      action: Event::CREATED,
+      target: @targets[project],
+      author: contributor,
+      created_at: day,
+    )
+  end
+
+  describe '#activity_dates' do
+    it "returns a hash of date => count" do
+      create_event(public_project, last_week)
+      create_event(public_project, last_week)
+      create_event(public_project, today)
+
+      expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
+    end
+
+    it "only shows private events to authorized users" do
+      create_event(private_project, today)
+      create_event(feature_project, today)
+
+      expect(calendar.activity_dates[today]).to eq(0)
+      expect(calendar(user).activity_dates[today]).to eq(0)
+      expect(calendar(contributor).activity_dates[today]).to eq(2)
+    end
+  end
+
+  describe '#events_by_date' do
+    it "returns all events for a given date" do
+      e1 = create_event(public_project, today)
+      e2 = create_event(public_project, today)
+      create_event(public_project, last_week)
+
+      expect(calendar.events_by_date(today)).to contain_exactly(e1, e2)
+    end
+
+    it "only shows private events to authorized users" do
+      e1 = create_event(public_project, today)
+      e2 = create_event(private_project, today)
+      e3 = create_event(feature_project, today)
+      create_event(public_project, last_week)
+
+      expect(calendar.events_by_date(today)).to contain_exactly(e1)
+      expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
+    end
+  end
+
+  describe '#starting_year' do
+    it "should be the start of last year" do
+      expect(calendar.starting_year).to eq(last_year.year)
+    end
+  end
+
+  describe '#starting_month' do
+    it "should be the start of this month" do
+      expect(calendar.starting_month).to eq(today.month)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index b73434e8dd787626cf95c50cf6b5080674de4c1a..a379f798a16c0227604a461ccde9aa616bba89ac 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -8,13 +8,13 @@ describe Gitlab::DataBuilder::Push, lib: true do
     let(:data) { described_class.build_sample(project, user) }
 
     it { expect(data).to be_a(Hash) }
-    it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
-    it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+    it { expect(data[:before]).to eq('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') }
+    it { expect(data[:after]).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
     it { expect(data[:ref]).to eq('refs/heads/master') }
     it { expect(data[:commits].size).to eq(3) }
     it { expect(data[:total_commits_count]).to eq(3) }
-    it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
-    it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
+    it { expect(data[:commits].first[:added]).to eq(['bar/branch-test.txt']) }
+    it { expect(data[:commits].first[:modified]).to eq([]) }
     it { expect(data[:commits].first[:removed]).to eq([]) }
 
     include_examples 'project hook data with deprecateds'
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ec3f19e03fb55bb2c3714718d8df5e4976e0c35..7fd25b9e5bf868c21876f17f15fc358a0628beef 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -91,63 +91,80 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
 
   describe '#add_column_with_default' do
     context 'outside of a transaction' do
-      before do
-        expect(model).to receive(:transaction_open?).and_return(false)
+      context 'when a column limit is not set' do
+        before do
+          expect(model).to receive(:transaction_open?).and_return(false)
 
-        expect(model).to receive(:transaction).and_yield
+          expect(model).to receive(:transaction).and_yield
 
-        expect(model).to receive(:add_column).
-          with(:projects, :foo, :integer, default: nil)
+          expect(model).to receive(:add_column).
+            with(:projects, :foo, :integer, default: nil)
 
-        expect(model).to receive(:change_column_default).
-          with(:projects, :foo, 10)
-      end
+          expect(model).to receive(:change_column_default).
+            with(:projects, :foo, 10)
+        end
 
-      it 'adds the column while allowing NULL values' do
-        expect(model).to receive(:update_column_in_batches).
-          with(:projects, :foo, 10)
+        it 'adds the column while allowing NULL values' do
+          expect(model).to receive(:update_column_in_batches).
+            with(:projects, :foo, 10)
 
-        expect(model).not_to receive(:change_column_null)
+          expect(model).not_to receive(:change_column_null)
 
-        model.add_column_with_default(:projects, :foo, :integer,
-                                      default: 10,
-                                      allow_null: true)
-      end
+          model.add_column_with_default(:projects, :foo, :integer,
+                                        default: 10,
+                                        allow_null: true)
+        end
 
-      it 'adds the column while not allowing NULL values' do
-        expect(model).to receive(:update_column_in_batches).
-          with(:projects, :foo, 10)
+        it 'adds the column while not allowing NULL values' do
+          expect(model).to receive(:update_column_in_batches).
+            with(:projects, :foo, 10)
 
-        expect(model).to receive(:change_column_null).
-          with(:projects, :foo, false)
+          expect(model).to receive(:change_column_null).
+            with(:projects, :foo, false)
 
-        model.add_column_with_default(:projects, :foo, :integer, default: 10)
-      end
+          model.add_column_with_default(:projects, :foo, :integer, default: 10)
+        end
 
-      it 'removes the added column whenever updating the rows fails' do
-        expect(model).to receive(:update_column_in_batches).
-          with(:projects, :foo, 10).
-          and_raise(RuntimeError)
+        it 'removes the added column whenever updating the rows fails' do
+          expect(model).to receive(:update_column_in_batches).
+            with(:projects, :foo, 10).
+            and_raise(RuntimeError)
 
-        expect(model).to receive(:remove_column).
-          with(:projects, :foo)
+          expect(model).to receive(:remove_column).
+            with(:projects, :foo)
 
-        expect do
-          model.add_column_with_default(:projects, :foo, :integer, default: 10)
-        end.to raise_error(RuntimeError)
+          expect do
+            model.add_column_with_default(:projects, :foo, :integer, default: 10)
+          end.to raise_error(RuntimeError)
+        end
+
+        it 'removes the added column whenever changing a column NULL constraint fails' do
+          expect(model).to receive(:change_column_null).
+            with(:projects, :foo, false).
+            and_raise(RuntimeError)
+
+          expect(model).to receive(:remove_column).
+            with(:projects, :foo)
+
+          expect do
+            model.add_column_with_default(:projects, :foo, :integer, default: 10)
+          end.to raise_error(RuntimeError)
+        end
       end
 
-      it 'removes the added column whenever changing a column NULL constraint fails' do
-        expect(model).to receive(:change_column_null).
-          with(:projects, :foo, false).
-          and_raise(RuntimeError)
+      context 'when a column limit is set' do
+        it 'adds the column with a limit' do
+          allow(model).to receive(:transaction_open?).and_return(false)
+          allow(model).to receive(:transaction).and_yield
+          allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10)
+          allow(model).to receive(:change_column_null).with(:projects, :foo, false)
+          allow(model).to receive(:change_column_default).with(:projects, :foo, 10)
 
-        expect(model).to receive(:remove_column).
-          with(:projects, :foo)
+          expect(model).to receive(:add_column).
+            with(:projects, :foo, :integer, default: nil, limit: 8)
 
-        expect do
-          model.add_column_with_default(:projects, :foo, :integer, default: 10)
-        end.to raise_error(RuntimeError)
+          model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+        end
       end
     end
 
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 10537bea00830e3d641998277be1be7080d49d5f..6e8fff6f5163c43589a580ca4a2440cbea0df8cb 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do
     end
   end
 
+  describe "position for a file in the initial commit" do
+    let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+    subject do
+      described_class.new(
+        old_path: "README.md",
+        new_path: "README.md",
+        old_line: nil,
+        new_line: 1,
+        diff_refs: commit.diff_refs
+      )
+    end
+
+    describe "#diff_file" do
+      it "returns the correct diff file" do
+        diff_file = subject.diff_file(project.repository)
+
+        expect(diff_file.new_file).to be true
+        expect(diff_file.new_path).to eq(subject.new_path)
+        expect(diff_file.diff_refs).to eq(subject.diff_refs)
+      end
+    end
+
+    describe "#diff_line" do
+      it "returns the correct diff line" do
+        diff_line = subject.diff_line(project.repository)
+
+        expect(diff_line.added?).to be true
+        expect(diff_line.new_line).to eq(subject.new_line)
+        expect(diff_line.text).to eq("+testme")
+      end
+    end
+
+    describe "#line_code" do
+      it "returns the correct line code" do
+        line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+        expect(subject.line_code(project.repository)).to eq(line_code)
+      end
+    end
+  end
+
   describe "#to_json" do
     let(:hash) do
       {
diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb
index 93094cda776f03f3e7b31a629c8b59f20192fc5d..a5a398abf78b73d8b0d7fd62a470958cfc791553 100644
--- a/spec/lib/gitlab/downtime_check/message_spec.rb
+++ b/spec/lib/gitlab/downtime_check/message_spec.rb
@@ -5,13 +5,35 @@ describe Gitlab::DowntimeCheck::Message do
     it 'returns an ANSI formatted String for an offline migration' do
       message = described_class.new('foo.rb', true, 'hello')
 
-      expect(message.to_s).to eq("[\e[32moffline\e[0m]: foo.rb: hello")
+      expect(message.to_s).to eq("[\e[31moffline\e[0m]: foo.rb:\n\nhello\n\n")
     end
 
     it 'returns an ANSI formatted String for an online migration' do
       message = described_class.new('foo.rb')
 
-      expect(message.to_s).to eq("[\e[31monline\e[0m]: foo.rb")
+      expect(message.to_s).to eq("[\e[32monline\e[0m]: foo.rb")
+    end
+  end
+
+  describe '#reason?' do
+    it 'returns false when no reason is specified' do
+      message = described_class.new('foo.rb')
+
+      expect(message.reason?).to eq(false)
+    end
+
+    it 'returns true when a reason is specified' do
+      message = described_class.new('foo.rb', true, 'hello')
+
+      expect(message.reason?).to eq(true)
+    end
+  end
+
+  describe '#reason' do
+    it 'strips excessive whitespace from the returned String' do
+      message = described_class.new('foo.rb', true, " hello\n world\n\n foo")
+
+      expect(message.reason).to eq("hello\nworld\n\nfoo")
     end
   end
 end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index e115315477822703498e05027098ed46dd17747e..cb3651e3845be2137f4be158b7ff56cdb822370f 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
     create(
       :user,
       email: 'jake@adventuretime.ooo',
-      authentication_token: 'auth_token'
+      incoming_email_token: 'auth_token'
     )
   end
 
@@ -60,8 +60,8 @@ describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
       end
     end
 
-    context "when we can't find the authentication_token" do
-      let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") }
+    context "when we can't find the incoming_email_token" do
+      let(:email_raw) { fixture_file("emails/wrong_incoming_email_token.eml") }
 
       it "raises an UserNotFoundError" do
         expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index a2119b0dadfb1619434cc6ffe1c60d33593c3a53..48660d1dd1b035c12e9a1da674e6bc9ef33a31d4 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -12,10 +12,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
 
   let(:email_raw) { fixture_file('emails/valid_reply.eml') }
   let(:project)   { create(:project, :public) }
-  let(:noteable)  { create(:issue, project: project) }
   let(:user)      { create(:user) }
+  let(:note)      { create(:diff_note_on_merge_request, project: project) }
+  let(:noteable)  { note.noteable }
 
-  let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+  let!(:sent_notification) do
+    SentNotification.record_note(note, user.id, mail_key)
+  end
 
   context "when the recipient address doesn't include a mail key" do
     let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") }
@@ -60,6 +63,64 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
     it "raises an InvalidNoteError" do
       expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
     end
+
+    context 'because the note was commands only' do
+      let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") }
+
+      context 'and current user cannot update noteable' do
+        it 'raises a CommandsOnlyNoteError' do
+          expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
+        end
+      end
+
+      context 'and current user can update noteable' do
+        before do
+          project.team << [user, :developer]
+        end
+
+        it 'does not raise an error' do
+          expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+          # One system note is created for the 'close' event
+          expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+          expect(noteable.reload).to be_closed
+          expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+        end
+      end
+    end
+  end
+
+  context 'when the note contains slash commands' do
+    let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
+
+    context 'and current user cannot update noteable' do
+      it 'post a note and does not update the noteable' do
+        expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+        # One system note is created for the new note
+        expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+        expect(noteable.reload).to be_open
+        expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+      end
+    end
+
+    context 'and current user can update noteable' do
+      before do
+        project.team << [user, :developer]
+      end
+
+      it 'post a note and updates the noteable' do
+        expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+        # One system note is created for the new note, one for the 'close' event
+        expect { receiver.execute }.to change { noteable.notes.count }.by(2)
+
+        expect(noteable.reload).to be_closed
+        expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+      end
+    end
   end
 
   context "when the reply is blank" do
@@ -77,10 +138,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
 
     it "creates a comment" do
       expect { receiver.execute }.to change { noteable.notes.count }.by(1)
-      note = noteable.notes.last
+      new_note = noteable.notes.last
 
-      expect(note.author).to eq(sent_notification.recipient)
-      expect(note.note).to include("I could not disagree more.")
+      expect(new_note.author).to eq(sent_notification.recipient)
+      expect(new_note.position).to eq(note.position)
+      expect(new_note.note).to include("I could not disagree more.")
     end
 
     it "adds all attachments" do
@@ -99,10 +161,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
       shared_examples 'an email that contains a mail key' do |header|
         it "fetches the mail key from the #{header} header and creates a comment" do
           expect { receiver.execute }.to change { noteable.notes.count }.by(1)
-          note = noteable.notes.last
+          new_note = noteable.notes.last
 
-          expect(note.author).to eq(sent_notification.recipient)
-          expect(note.note).to include('I could not disagree more.')
+          expect(new_note.author).to eq(sent_notification.recipient)
+          expect(new_note.position).to eq(note.position)
+          expect(new_note.note).to include('I could not disagree more.')
         end
       end
 
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index fbdb7ea34ac531311240eb53ea0c7c1c833dd607..a366d68a1460772624a89d04455f43ce6605b13b 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -1,21 +1,51 @@
 require 'spec_helper'
 
-describe Gitlab::ExclusiveLease do
-  it 'cannot obtain twice before the lease has expired' do
-    lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
-    expect(lease.try_obtain).to eq(true)
-    expect(lease.try_obtain).to eq(false)
+describe Gitlab::ExclusiveLease, type: :redis do
+  let(:unique_key) { SecureRandom.hex(10) }
+
+  describe '#try_obtain' do
+    it 'cannot obtain twice before the lease has expired' do
+      lease = described_class.new(unique_key, timeout: 3600)
+      expect(lease.try_obtain).to be_present
+      expect(lease.try_obtain).to eq(false)
+    end
+
+    it 'can obtain after the lease has expired' do
+      timeout = 1
+      lease = described_class.new(unique_key, timeout: timeout)
+      lease.try_obtain # start the lease
+      sleep(2 * timeout) # lease should have expired now
+      expect(lease.try_obtain).to be_present
+    end
   end
 
-  it 'can obtain after the lease has expired' do
-    timeout = 1
-    lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
-    lease.try_obtain # start the lease
-    sleep(2 * timeout) # lease should have expired now
-    expect(lease.try_obtain).to eq(true)
+  describe '#exists?' do
+    it 'returns true for an existing lease' do
+      lease = described_class.new(unique_key, timeout: 3600)
+      lease.try_obtain
+
+      expect(lease.exists?).to eq(true)
+    end
+
+    it 'returns false for a lease that does not exist' do
+      lease = described_class.new(unique_key, timeout: 3600)
+
+      expect(lease.exists?).to eq(false)
+    end
   end
 
-  def unique_key
-    SecureRandom.hex(10)
+  describe '.cancel' do
+    it 'can cancel a lease' do
+      uuid = new_lease(unique_key)
+      expect(uuid).to be_present
+      expect(new_lease(unique_key)).to eq(false)
+
+      described_class.cancel(unique_key, uuid)
+      expect(new_lease(unique_key)).to be_present
+    end
+
+    def new_lease(key)
+      described_class.new(key, timeout: 3600).try_obtain
+    end
   end
 end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 0af249d8690edb80bceaae9fb7bf6b22323cc2d5..6b3dfebd85d9308caaf733a89d945cbcc37f1357 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
 
 describe Gitlab::Gfm::ReferenceRewriter do
   let(:text) { 'some text' }
-  let(:old_project) { create(:project) }
-  let(:new_project) { create(:project) }
+  let(:old_project) { create(:project, name: 'old') }
+  let(:new_project) { create(:project, name: 'new') }
   let(:user) { create(:user) }
 
-  before { old_project.team << [user, :guest] }
+  before { old_project.team << [user, :reporter] }
 
   describe '#rewrite' do
     subject do
@@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
           it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
         end
 
-        context 'description with labels' do
+        context 'description with project labels' do
           let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
           let(:project_ref) { old_project.to_reference }
 
@@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do
             it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
           end
         end
+
+        context 'description with group labels' do
+          let(:old_group) { create(:group) }
+          let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
+          let(:project_ref) { old_project.to_reference }
+
+          before do
+            old_project.update(namespace: old_group)
+          end
+
+          context 'label referenced by id' do
+            let(:text) { '#1 and ~321' }
+            it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+          end
+
+          context 'label referenced by text' do
+            let(:text) { '#1 and ~"group label"' }
+            it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+          end
+        end
       end
 
       context 'reference contains milestone' do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index d0e73d70e6e68035c555c5767a3ae8a8b6a2db4e..502ee9ce2093a8c21f1046652d558c121fa2483b 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,10 +1,17 @@
 require 'spec_helper'
 
 describe Gitlab::GitAccess, lib: true do
-  let(:access) { Gitlab::GitAccess.new(actor, project, 'web') }
+  let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
   let(:project) { create(:project) }
   let(:user) { create(:user) }
   let(:actor) { user }
+  let(:authentication_abilities) do
+    [
+      :read_project,
+      :download_code,
+      :push_code
+    ]
+  end
 
   describe '#check with single protocols allowed' do
     def disable_protocol(protocol)
@@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do
     context 'ssh disabled' do
       before do
         disable_protocol('ssh')
-        @acc = Gitlab::GitAccess.new(actor, project, 'ssh')
+        @acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
       end
 
       it 'blocks ssh git push' do
@@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do
     context 'http disabled' do
       before do
         disable_protocol('http')
-        @acc = Gitlab::GitAccess.new(actor, project, 'http')
+        @acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
       end
 
       it 'blocks http push' do
@@ -59,6 +66,7 @@ describe Gitlab::GitAccess, lib: true do
 
       context 'pull code' do
         it { expect(subject.allowed?).to be_falsey }
+        it { expect(subject.message).to match(/You are not allowed to download code/) }
       end
     end
 
@@ -70,6 +78,7 @@ describe Gitlab::GitAccess, lib: true do
 
       context 'pull code' do
         it { expect(subject.allowed?).to be_falsey }
+        it { expect(subject.message).to match(/Your account has been blocked/) }
       end
     end
 
@@ -77,6 +86,29 @@ describe Gitlab::GitAccess, lib: true do
       context 'pull code' do
         it { expect(subject.allowed?).to be_falsey }
       end
+
+      context 'when project is public' do
+        let(:public_project) { create(:project, :public) }
+        let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
+        subject { guest_access.check('git-upload-pack', '_any') }
+
+        context 'when repository is enabled' do
+          it 'give access to download code' do
+            public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+            expect(subject.allowed?).to be_truthy
+          end
+        end
+
+        context 'when repository is disabled' do
+          it 'does not give access to download code' do
+            public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+            expect(subject.allowed?).to be_falsey
+            expect(subject.message).to match(/You are not allowed to download code/)
+          end
+        end
+      end
     end
 
     describe 'deploy key permissions' do
@@ -111,6 +143,44 @@ describe Gitlab::GitAccess, lib: true do
         end
       end
     end
+
+    describe 'build authentication_abilities permissions' do
+      let(:authentication_abilities) { build_authentication_abilities }
+
+      describe 'owner' do
+        let(:project) { create(:project, namespace: user.namespace) }
+
+        context 'pull code' do
+          it { expect(subject).to be_allowed }
+        end
+      end
+
+      describe 'reporter user' do
+        before { project.team << [user, :reporter] }
+
+        context 'pull code' do
+          it { expect(subject).to be_allowed }
+        end
+      end
+
+      describe 'admin user' do
+        let(:user) { create(:admin) }
+
+        context 'when member of the project' do
+          before { project.team << [user, :reporter] }
+
+          context 'pull code' do
+            it { expect(subject).to be_allowed }
+          end
+        end
+
+        context 'when is not member of the project' do
+          context 'pull code' do
+            it { expect(subject).not_to be_allowed }
+          end
+        end
+      end
+    end
   end
 
   describe 'push_access_check' do
@@ -148,6 +218,7 @@ describe Gitlab::GitAccess, lib: true do
       end
     end
 
+    # Run permission checks for a user
     def self.run_permission_checks(permissions_matrix)
       permissions_matrix.keys.each do |role|
         describe "#{role} access" do
@@ -157,13 +228,12 @@ 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.push_access_check(changes[action]) }
-
-              it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+            permissions_matrix[role].each do |action, allowed|
+              context action do
+                subject { access.push_access_check(changes[action]) }
+                it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+              end
             end
           end
         end
@@ -283,41 +353,71 @@ describe Gitlab::GitAccess, lib: true do
     end
   end
 
-  describe 'deploy key permissions' do
-    context 'push code' do
-      subject { access.check('git-receive-pack', '_any') }
+  shared_examples 'can not push code' do
+    subject { access.check('git-receive-pack', '_any') }
 
-      context 'when project is authorized' do
-        let(:key) { create(:deploy_key, can_push: true) }
-        let(:actor) { key }
+    context 'when project is authorized' do
+      before { authorize }
+
+      it { expect(subject).not_to be_allowed }
+    end
 
-        before { key.projects << project }
+    context 'when unauthorized' do
+      context 'to public project' do
+        let(:project) { create(:project, :public) }
 
         it { expect(subject).to be_allowed }
       end
 
-      context 'when unauthorized' do
-        let(:key) { create(:deploy_key, can_push: false) }
-        let(:actor) { key }
+      context 'to internal project' do
+        let(:project) { create(:project, :internal) }
 
-        context 'to public project' do
-          let(:project) { create(:project, :public) }
+        it { expect(subject).not_to be_allowed }
+      end
 
-          it { expect(subject).not_to be_allowed }
-        end
+      context 'to private project' do
+        let(:project) { create(:project) }
+
+        it { expect(subject).not_to be_allowed }
+      end
+    end
+  end
 
-        context 'to internal project' do
-          let(:project) { create(:project, :internal) }
+  describe 'build authentication abilities' do
+    let(:authentication_abilities) { build_authentication_abilities }
 
-          it { expect(subject).not_to be_allowed }
-        end
+    it_behaves_like 'can not push code' do
+      def authorize
+        project.team << [user, :reporter]
+      end
+    end
+  end
 
-        context 'to private project' do
-          let(:project) { create(:project, :internal) }
+  describe 'deploy key permissions' do
+    let(:key) { create(:deploy_key) }
+    let(:actor) { key }
 
-          it { expect(subject).not_to be_allowed }
-        end
+    it_behaves_like 'can not push code' do
+      def authorize
+        key.projects << project
       end
     end
   end
+
+  private
+
+  def build_authentication_abilities
+    [
+      :read_project,
+      :build_download_code
+    ]
+  end
+
+  def full_authentication_abilities
+    [
+      :read_project,
+      :download_code,
+      :push_code
+    ]
+  end
 end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4244b807d416a074c11383bcc361502632e7f6ec..576aa5c366fd32e3a920ad5cb50d35d2adc87f93 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,9 +1,16 @@
 require 'spec_helper'
 
 describe Gitlab::GitAccessWiki, lib: true do
-  let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') }
+  let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) }
   let(:project) { create(:project) }
   let(:user) { create(:user) }
+  let(:authentication_abilities) do
+    [
+      :read_project,
+      :download_code,
+      :push_code
+    ]
+  end
 
   describe 'push_allowed?' do
     before do
@@ -11,7 +18,7 @@ describe Gitlab::GitAccessWiki, lib: true do
       project.team << [user, :developer]
     end
 
-    subject { access.push_access_check(changes) }
+    subject { access.check('git-receive-pack', changes) }
 
     it { expect(subject.allowed?).to be_truthy }
   end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..219198eff60f5d203ff0c89ff6246b861f407c8f
--- /dev/null
+++ b/spec/lib/gitlab/git_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Git, lib: true do
+  let(:committer_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.")
+  # This seems to be what git does under the hood. For example, this commit:
+  #
+  # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+  #
+  # results in this:
+  #
+  # $ git show --pretty
+  # ...
+  # Author: Foo Sr <foo@example.com>
+  # ...
+  let(:committer_name) { FFaker::Name.name.chomp("\.") }
+
+  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)
+
+      expect(committer_hash[:email]).to eq(committer_email)
+      expect(committer_hash[:name]).to eq(committer_name)
+      expect(committer_hash[:time]).to be_a(Time)
+    end
+
+    context 'when email is nil' do
+      it "returns nil" do
+        committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+
+        expect(committer_hash).to be_nil
+      end
+    end
+
+    context 'when name is nil' do
+      it "returns nil" do
+        committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+
+        expect(committer_hash).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 613c47d55f17ee0f23f667d7a01f0e8cdf708d80..e829b93634333a21234f841c4a300ba3e18f288c 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -66,6 +66,6 @@ describe Gitlab::GithubImport::Client, lib: true do
     stub_request(:get, /api.github.com/)
     allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
 
-    expect { client.issues }.not_to raise_error
+    expect { client.issues {} }.not_to raise_error
   end
 end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index 9ae02a6c45fbb047f08a2b4836e1af86d2f68c32..c520a9c53ad7d4253b9acf6acecd6d2037e3e3e6 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -73,6 +73,12 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
         gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
         expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
       end
+
+      it 'returns note without created at tag line' do
+        create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+        expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.")
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7478f86bd2831bd23b56c4484160a714bd7cc9f5
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer, lib: true do
+  describe '#execute' do
+    before do
+      allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+    end
+
+    context 'when an error occurs' do
+      let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
+      let(:octocat) { double(id: 123456, login: 'octocat') }
+      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: '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(:label1) do
+        double(
+          name: 'Bug',
+          color: 'ff0000',
+          url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+        )
+      end
+
+      let(:label2) do
+        double(
+          name: nil,
+          color: 'ff0000',
+          url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+        )
+      end
+
+      let(:milestone) do
+        double(
+          number: 1347,
+          state: 'open',
+          title: '1.0',
+          description: 'Version 1.0',
+          due_on: nil,
+          created_at: created_at,
+          updated_at: updated_at,
+          closed_at: nil,
+          url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
+        )
+      end
+
+      let(:issue1) do
+        double(
+          number: 1347,
+          milestone: nil,
+          state: 'open',
+          title: 'Found a bug',
+          body: "I'm having a problem with this.",
+          assignee: nil,
+          user: octocat,
+          comments: 0,
+          pull_request: nil,
+          created_at: created_at,
+          updated_at: updated_at,
+          closed_at: nil,
+          url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
+          labels: [double(name: 'Label #1')],
+        )
+      end
+
+      let(:issue2) do
+        double(
+          number: 1348,
+          milestone: nil,
+          state: 'open',
+          title: nil,
+          body: "I'm having a problem with this.",
+          assignee: nil,
+          user: octocat,
+          comments: 0,
+          pull_request: nil,
+          created_at: created_at,
+          updated_at: updated_at,
+          closed_at: nil,
+          url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
+          labels: [double(name: 'Label #2')],
+        )
+      end
+
+      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: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
+          labels: [double(name: 'Label #3')],
+        )
+      end
+
+      let(:release1) do
+        double(
+          tag_name: 'v1.0.0',
+          name: 'First release',
+          body: 'Release v1.0.0',
+          draft: false,
+          created_at: created_at,
+          updated_at: updated_at,
+          url: 'https://api.github.com/repos/octocat/Hello-World/releases/1'
+        )
+      end
+
+      let(:release2) do
+        double(
+          tag_name: 'v2.0.0',
+          name: 'Second release',
+          body: nil,
+          draft: false,
+          created_at: created_at,
+          updated_at: updated_at,
+          url: 'https://api.github.com/repos/octocat/Hello-World/releases/2'
+        )
+      end
+
+      before do
+        allow(project).to receive(:import_data).and_return(double.as_null_object)
+        allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+        allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
+        allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+        allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+        allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+        allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
+        allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
+        allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+        allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
+        allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+      end
+
+      it 'returns true' do
+        expect(described_class.new(project).execute).to eq true
+      end
+
+      it 'does not raise an error' do
+        expect { described_class.new(project).execute }.not_to raise_error
+      end
+
+      it 'stores error messages' do
+        error = {
+          message: 'The remote data could not be fully imported.',
+          errors: [
+            { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
+            { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
+            { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+            { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+            { type: :wiki, errors: "Gitlab::Shell::Error" },
+            { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
+          ]
+        }
+
+        described_class.new(project).execute
+
+        expect(project.import_error).to eq error.to_json
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index 0e7ffbe9b8eb3dfcd578729e3df2f7a612197c32..c2f1f6b91a112a08e89555243d2194c11c0ca971 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
     end
 
     context 'when issue is closed' do
-      let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
-      let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+      let(:raw_data) { double(base_data.merge(state: 'closed')) }
 
       it 'returns formatted attributes' do
         expected = {
@@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
           author_id: project.creator_id,
           assignee_id: nil,
           created_at: created_at,
-          updated_at: closed_at
+          updated_at: updated_at
         }
 
         expect(issue.attributes).to eq(expected)
@@ -110,6 +109,12 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
 
         expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
       end
+
+      it 'returns description without created at tag line' do
+        create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+        expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.")
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index 87593e32db0dc9282d240b1fcef84d0ea4dfad66..8098754d735f775b321b88710528ca8933fd2024 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -1,18 +1,34 @@
 require 'spec_helper'
 
 describe Gitlab::GithubImport::LabelFormatter, lib: true do
-  describe '#attributes' do
-    it 'returns formatted attributes' do
-      project = create(:project)
-      raw = double(name: 'improvements', color: 'e6e6e6')
+  let(:project) { create(:project) }
+  let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
 
-      formatter = described_class.new(project, raw)
+  subject { described_class.new(project, raw) }
 
-      expect(formatter.attributes).to eq({
+  describe '#attributes' do
+    it 'returns formatted attributes' do
+      expect(subject.attributes).to eq({
         project: project,
         title: 'improvements',
         color: '#e6e6e6'
       })
     end
   end
+
+  describe '#create!' do
+    context 'when label does not exist' do
+      it 'creates a new label' do
+        expect { subject.create! }.to change(Label, :count).by(1)
+      end
+    end
+
+    context 'when label exists' do
+      it 'does not create a new label' do
+        project.labels.create(name: raw.name)
+
+        expect { subject.create! }.not_to change(Label, :count)
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
index 5a421e505811167bec0f3d2cb1d79c1aad429666..09337c99a07952c65ad85d34e2df2d6ce7cd9d81 100644
--- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
@@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
     end
 
     context 'when milestone is closed' do
-      let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
-      let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+      let(:raw_data) { double(base_data.merge(state: 'closed')) }
 
       it 'returns formatted attributes' do
         expected = {
@@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
           state: 'closed',
           due_date: nil,
           created_at: created_at,
-          updated_at: closed_at
+          updated_at: updated_at
         }
 
         expect(formatter.attributes).to eq(expected)
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 0f363b8b0aaa6db109ce6d01ebfe39de5a1689bb..a73b1f4ff5d191bea63831e5a4231af3fe70d6a0 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -2,33 +2,79 @@ require 'spec_helper'
 
 describe Gitlab::GithubImport::ProjectCreator, lib: true do
   let(:user) { create(:user) }
+  let(:namespace) { create(:group, owner: user) }
+
   let(:repo) do
     OpenStruct.new(
       login: 'vim',
       name: 'vim',
-      private: true,
       full_name: 'asd/vim',
-      clone_url: "https://gitlab.com/asd/vim.git",
-      owner: OpenStruct.new(login: "john")
+      clone_url: 'https://gitlab.com/asd/vim.git'
     )
   end
-  let(:namespace) { create(:group, owner: user) }
-  let(:token) { "asdffg" }
-  let(:access_params) { { github_access_token: token } }
+
+  subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
 
   before do
     namespace.add_owner(user)
+    allow_any_instance_of(Project).to receive(:add_import_job)
   end
 
-  it 'creates project' do
-    allow_any_instance_of(Project).to receive(:add_import_job)
+  describe '#execute' do
+    it 'creates a project' do
+      expect { service.execute }.to change(Project, :count).by(1)
+    end
+
+    it 'handle GitHub credentials' do
+      project = service.execute
+
+      expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git')
+      expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git')
+      expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
+    end
+
+    context 'when GitHub project is private' do
+      it 'sets project visibility to private' do
+        repo.private = true
+
+        project = service.execute
+
+        expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+      end
+    end
+
+    context 'when GitHub project is public' do
+      before do
+        allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
+      end
+
+      it 'sets project visibility to the default project visibility' do
+        repo.private = false
+
+        project = service.execute
+
+        expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+      end
+    end
+
+    context 'when GitHub project has wiki' do
+      it 'does not create the wiki repository' do
+        allow(repo).to receive(:has_wiki?).and_return(true)
+
+        project = service.execute
+
+        expect(project.wiki.repository_exists?).to eq false
+      end
+    end
+
+    context 'when GitHub project does not have wiki' do
+      it 'creates the wiki repository' do
+        allow(repo).to receive(:has_wiki?).and_return(false)
 
-    project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params)
-    project = project_creator.execute
+        project = service.execute
 
-    expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
-    expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git")
-    expect(project.import_data.credentials).to eq(user: "asdffg", password: nil)
-    expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+        expect(project.wiki.repository_exists?).to eq true
+      end
+    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 aa28e360993434ba264a3362a65a4d5f5ac07e75..302f0fc06236fbffeeb5a95001ba71d518d50f61 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -27,7 +27,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       created_at: created_at,
       updated_at: updated_at,
       closed_at: nil,
-      merged_at: nil
+      merged_at: nil,
+      url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
     }
   end
 
@@ -61,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
     end
 
     context 'when pull request is closed' do
-      let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
-      let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) }
+      let(:raw_data) { double(base_data.merge(state: 'closed')) }
 
       it 'returns formatted attributes' do
         expected = {
@@ -80,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
           author_id: project.creator_id,
           assignee_id: nil,
           created_at: created_at,
-          updated_at: closed_at
+          updated_at: updated_at
         }
 
         expect(pull_request.attributes).to eq(expected)
@@ -107,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
           author_id: project.creator_id,
           assignee_id: nil,
           created_at: created_at,
-          updated_at: merged_at
+          updated_at: updated_at
         }
 
         expect(pull_request.attributes).to eq(expected)
@@ -140,6 +140,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
 
         expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
       end
+
+      it 'returns description without created at tag line' do
+        create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+        expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes')
+      end
     end
 
     context 'when it has a milestone' do
@@ -229,4 +235,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
       end
     end
   end
+
+  describe '#url' do
+    let(:raw_data) { double(base_data) }
+
+    it 'return raw url' do
+      expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+    end
+  end
 end
diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/github_import/release_formatter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..793128c6ab92b271ec401950f9b786fd8d5a6e9b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/release_formatter_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ReleaseFormatter, lib: true do
+  let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+  let(:octocat) { double(id: 123456, login: 'octocat') }
+  let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+
+  let(:base_data) do
+    {
+      tag_name: 'v1.0.0',
+      name: 'First release',
+      draft: false,
+      created_at: created_at,
+      published_at: created_at,
+      body: 'Release v1.0.0'
+    }
+  end
+
+  subject(:release) { described_class.new(project, raw_data) }
+
+  describe '#attributes' do
+    let(:raw_data) { double(base_data) }
+
+    it 'returns formatted attributes' do
+      expected = {
+        project: project,
+        tag: 'v1.0.0',
+        description: 'Release v1.0.0',
+        created_at: created_at,
+        updated_at: created_at
+      }
+
+      expect(release.attributes).to eq(expected)
+    end
+  end
+
+  describe '#valid' do
+    context 'when release is not a draft' do
+      let(:raw_data) { double(base_data) }
+
+      it 'returns true' do
+        expect(release.valid?).to eq true
+      end
+    end
+
+    context 'when release is draft' do
+      let(:raw_data) { double(base_data.merge(draft: true)) }
+
+      it 'returns false' do
+        expect(release.valid?).to eq false
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
index d3f1deb383765b221f5f5faaeb60a23d1d5d052e..9b499b593d32ced37b99ab25a72b56699ac2c1df 100644
--- a/spec/lib/gitlab/gitlab_import/importer_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
           'title' => 'Issue',
           'description' => 'Lorem ipsum',
           'state' => 'opened',
+          'confidential' => true,
           'author' => {
             'id' => 283999,
             'name' => 'John Doe'
@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
         title: 'Issue',
         description: "*Created by: John Doe*\n\nLorem ipsum",
         state: 'opened',
+        confidential: true,
         author_id: project.creator_id
       }
 
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
deleted file mode 100644
index 946712ca38eb9dfdcacfc3f1382f8e63afe3a751..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
-  let(:user) { create(:user) }
-  let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
-  let(:namespace){ create(:group, owner: user) }
-
-  before do
-    namespace.add_owner(user)
-  end
-
-  it 'creates project' do
-    allow_any_instance_of(Project).to receive(:add_import_job)
-
-    project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
-    project = project_creator.execute
-
-    expect(project.name).to eq("Bar Baz Qux")
-    expect(project.path).to eq("bar-baz-qux")
-    expect(project.namespace).to eq(namespace)
-    expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
-    expect(project.import_type).to eq("gitorious")
-    expect(project.import_source).to eq("foo/bar-baz-qux")
-    expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git")
-  end
-end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 54f85f8cffc699b19249a77696ad98e1c06a562c..097861fd34db95f76b64710e5e1acc351fb527be 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
   subject { described_class.new(project) }
 
   before do
+    project.team << [project.creator, :master]
     project.create_import_data(data: import_data)
   end
 
@@ -31,9 +32,9 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
       subject.execute
 
       %w(
-        Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical 
-        Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security 
-        Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery 
+        Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
+        Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
+        Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
         Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
       ).each do |label|
         label.sub!("-", ": ")
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..47d6f1007d1b8b2b8daae451c2e55ef218ee8e5b
--- /dev/null
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Gitlab::Identifier do
+  let(:identifier) do
+    Class.new { include Gitlab::Identifier }.new
+  end
+
+  let(:project) { create(:empty_project) }
+  let(:user) { create(:user) }
+  let(:key) { create(:key, user: user) }
+
+  describe '#identify' do
+    context 'without an identifier' do
+      it 'identifies the user using a commit' do
+        expect(identifier).to receive(:identify_using_commit).
+          with(project, '123')
+
+        identifier.identify('', project, '123')
+      end
+    end
+
+    context 'with a user identifier' do
+      it 'identifies the user using a user ID' do
+        expect(identifier).to receive(:identify_using_user).
+          with("user-#{user.id}")
+
+        identifier.identify("user-#{user.id}", project, '123')
+      end
+    end
+
+    context 'with an SSH key identifier' do
+      it 'identifies the user using an SSH key ID' do
+        expect(identifier).to receive(:identify_using_ssh_key).
+          with("key-#{key.id}")
+
+        identifier.identify("key-#{key.id}", project, '123')
+      end
+    end
+  end
+
+  describe '#identify_using_commit' do
+    it "returns the User for an existing commit author's Email address" do
+      commit = double(:commit, author_email: user.email)
+
+      expect(project).to receive(:commit).with('123').and_return(commit)
+
+      expect(identifier.identify_using_commit(project, '123')).to eq(user)
+    end
+
+    it 'returns nil when no user could be found' do
+      allow(project).to receive(:commit).with('123').and_return(nil)
+
+      expect(identifier.identify_using_commit(project, '123')).to be_nil
+    end
+
+    it 'returns nil when the commit does not have an author Email' do
+      commit = double(:commit, author_email: nil)
+
+      expect(project).to receive(:commit).with('123').and_return(commit)
+
+      expect(identifier.identify_using_commit(project, '123')).to be_nil
+    end
+
+    it 'caches the found users per Email' do
+      commit = double(:commit, author_email: user.email)
+
+      expect(project).to receive(:commit).with('123').twice.and_return(commit)
+      expect(User).to receive(:find_by).once.and_call_original
+
+      2.times do
+        expect(identifier.identify_using_commit(project, '123')).to eq(user)
+      end
+    end
+  end
+
+  describe '#identify_using_user' do
+    it 'returns the User for an existing ID in the identifier' do
+      found = identifier.identify_using_user("user-#{user.id}")
+
+      expect(found).to eq(user)
+    end
+
+    it 'returns nil for a non existing user ID' do
+      found = identifier.identify_using_user('user--1')
+
+      expect(found).to be_nil
+    end
+
+    it 'caches the found users per ID' do
+      expect(User).to receive(:find_by).once.and_call_original
+
+      2.times do
+        found = identifier.identify_using_user("user-#{user.id}")
+
+        expect(found).to eq(user)
+      end
+    end
+  end
+
+  describe '#identify_using_ssh_key' do
+    it 'returns the User for an existing SSH key' do
+      found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+      expect(found).to eq(user)
+    end
+
+    it 'returns nil for an invalid SSH key' do
+      found = identifier.identify_using_ssh_key('key--1')
+
+      expect(found).to be_nil
+    end
+
+    it 'caches the found users per key' do
+      expect(User).to receive(:find_by_ssh_key_id).once.and_call_original
+
+      2.times do
+        found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+        expect(found).to eq(user)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
new file mode 100644
index 0000000000000000000000000000000000000000..02b11bd999a695961adb7e0deb61bbc997fe9b01
--- /dev/null
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -0,0 +1,191 @@
+---
+issues:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- user_agent_detail
+- moved_to
+- events
+- merge_requests_closing_issues
+- metrics
+events:
+- author
+- project
+- target
+notes:
+- award_emoji
+- project
+- noteable
+- author
+- updated_by
+- resolved_by
+- todos
+- events
+label_links:
+- target
+- label
+label:
+- subscriptions
+- project
+- lists
+- label_links
+- issues
+- merge_requests
+- priorities
+milestone:
+- project
+- issues
+- labels
+- merge_requests
+- participants
+- events
+snippets:
+- author
+- project
+- notes
+- award_emoji
+releases:
+- project
+project_members:
+- created_by
+- user
+- source
+- project
+merge_requests:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- target_project
+- source_project
+- merge_user
+- merge_request_diffs
+- merge_request_diff
+- events
+- merge_requests_closing_issues
+- metrics
+merge_request_diff:
+- merge_request
+pipelines:
+- project
+- user
+- statuses
+- builds
+- trigger_requests
+statuses:
+- project
+- pipeline
+- user
+variables:
+- project
+triggers:
+- project
+- trigger_requests
+deploy_keys:
+- user
+- deploy_keys_projects
+- projects
+services:
+- project
+- service_hook
+hooks:
+- project
+protected_branches:
+- project
+- merge_access_levels
+- push_access_levels
+merge_access_levels:
+- protected_branch
+push_access_levels:
+- protected_branch
+project:
+- taggings
+- base_tags
+- tag_taggings
+- tags
+- creator
+- group
+- namespace
+- boards
+- last_event
+- services
+- campfire_service
+- drone_ci_service
+- emails_on_push_service
+- builds_email_service
+- pipelines_email_service
+- irker_service
+- pivotaltracker_service
+- hipchat_service
+- flowdock_service
+- assembla_service
+- asana_service
+- gemnasium_service
+- slack_service
+- buildkite_service
+- bamboo_service
+- teamcity_service
+- pushover_service
+- jira_service
+- redmine_service
+- custom_issue_tracker_service
+- bugzilla_service
+- gitlab_issue_tracker_service
+- external_wiki_service
+- forked_project_link
+- forked_from_project
+- forked_project_links
+- forks
+- merge_requests
+- fork_merge_requests
+- issues
+- labels
+- events
+- milestones
+- notes
+- snippets
+- hooks
+- protected_branches
+- project_members
+- users
+- requesters
+- deploy_keys_projects
+- deploy_keys
+- users_star_projects
+- starrers
+- releases
+- lfs_objects_projects
+- lfs_objects
+- project_group_links
+- invited_groups
+- todos
+- notification_settings
+- import_data
+- commit_statuses
+- pipelines
+- builds
+- runner_projects
+- runners
+- variables
+- triggers
+- environments
+- deployments
+- project_feature
+award_emoji:
+- awardable
+- user
+priorities:
+- label
\ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63bab0f0d0d261fefc8a56d9072b1814c325fdeb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AttributeCleaner, lib: true do
+  let(:relation_class){ double('relation_class').as_null_object }
+  let(:unsafe_hash) do
+    {
+      'id' => 101,
+      'service_id' => 99,
+      'moved_to_id' => 99,
+      'namespace_id' => 99,
+      'ci_id' => 99,
+      'random_project_id' => 99,
+      'random_id' => 99,
+      'milestone_id' => 99,
+      'project_id' => 99,
+      'user_id' => 99,
+      'random_id_in_the_middle' => 99,
+      'notid' => 99
+    }
+  end
+
+  let(:post_safe_hash) do
+    {
+      'project_id' => 99,
+      'user_id' => 99,
+      'random_id_in_the_middle' => 99,
+      'notid' => 99
+    }
+  end
+
+  it 'removes unwanted attributes from the hash' do
+    # allow(relation_class).to receive(:attribute_method?).and_return(true)
+    parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class)
+
+    expect(parsed_hash).to eq(post_safe_hash)
+  end
+end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea65a5dfed11a4c8e4446f5499717255a19d1c0d
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Checks whether there are new attributes in models that are currently being exported as part of the
+# project Import/Export feature.
+# If there are new attributes, these will have to either be added to this spec in case we want them
+# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
+# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes
+# to this spec.
+describe 'Import/Export attribute configuration', lib: true do
+  include ConfigurationHelper
+
+  let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+  let(:relation_names) do
+    names = names_from_tree(config_hash['project_tree'])
+
+    # 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']
+  end
+
+  let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+  let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+
+  it 'has no new columns' do
+    relation_names.each do |relation_name|
+      relation_class = relation_class_for_name(relation_name)
+      relation_attributes = relation_class.new.attributes.keys
+
+      expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
+
+      current_attributes = parsed_attributes(relation_name, relation_attributes)
+      safe_attributes = safe_model_attributes[relation_class.to_s]
+      new_attributes = current_attributes - safe_attributes
+
+      expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
+    end
+  end
+
+  def failure_message(relation_class, new_attributes)
+    <<-MSG
+      It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
+
+      Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+      Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
+      model in the +excluded_attributes+ section.
+
+      SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)}
+      IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+    MSG
+  end
+
+  class Author < User
+  end
+end
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a88ddd17aca69356ca3745466c8806d5a45825b9
--- /dev/null
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -0,0 +1,42 @@
+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(: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" }
+
+  before do
+    stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0)
+    allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+    allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true)
+
+    setup_files
+
+    described_class.import(archive_file: '', shared: shared)
+  end
+
+  after do
+    FileUtils.rm_rf(export_path)
+  end
+
+  it 'removes symlinks in root folder' do
+    expect(File.exist?(symlink_file)).to be false
+  end
+
+  it 'removes symlinks in subfolders' do
+    expect(File.exist?(subfolder_symlink_file)).to be false
+  end
+
+  it 'does not remove a valid file' do
+    expect(File.exist?(valid_file)).to be true
+  end
+
+  def setup_files
+    FileUtils.mkdir_p("#{shared.export_path}/subfolder/")
+    FileUtils.touch(valid_file)
+    FileUtils.ln_s(valid_file, symlink_file)
+    FileUtils.ln_s(valid_file, subfolder_symlink_file)
+  end
+end
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b492d1b9c7b4a0e24c73ed84639b5d151588154
--- /dev/null
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Finds if a new model has been added that can potentially be part of the Import/Export
+# If it finds a new model, it will show a +failure_message+ with the options available.
+describe 'Import/Export model configuration', lib: true do
+  include ConfigurationHelper
+
+  let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+  let(:model_names) do
+    names = names_from_tree(config_hash['project_tree'])
+
+    # 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.
+    # - User, Author... Models we do not care about for checking models
+    names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+  end
+
+  let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
+  let(:all_models) { YAML.load_file(all_models_yml) }
+  let(:current_models) { setup_models }
+
+  it 'has no new models' do
+    model_names.each do |model_name|
+      new_models = Array(current_models[model_name]) - Array(all_models[model_name])
+      expect(new_models).to be_empty, failure_message(model_name.classify, new_models)
+    end
+  end
+
+  # List of current models between models, in the format of
+  # {model: [model_2, model3], ...}
+  def setup_models
+    all_models_hash = {}
+
+    model_names.each do |model_name|
+      model_class = relation_class_for_name(model_name)
+
+      all_models_hash[model_name] = associations_for(model_class) - ['project']
+    end
+
+    all_models_hash
+  end
+
+  def failure_message(parent_model_name, new_models)
+    <<-MSG
+      New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by
+      the Import/Export feature.
+
+      If you think this model should be included in the export, please add it to IMPORT_EXPORT_CONFIG.
+      Definitely add it to MODELS_JSON to signal that you've handled this error and to prevent it from showing up in the future.
+
+      MODELS_JSON: #{File.expand_path(all_models_yml)}
+      IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+    MSG
+  end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index cbbf98dca94f36490dc0e91d5b2c7cb32f17e859..ed9df468cede2a66f28fa23296655a79b553eb0c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -1,11 +1,22 @@
 {
   "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
-  "issues_enabled": true,
-  "merge_requests_enabled": true,
-  "wiki_enabled": true,
-  "snippets_enabled": false,
   "visibility_level": 10,
   "archived": false,
+  "labels": [
+    {
+      "id": 2,
+      "title": "test2",
+      "color": "#428bca",
+      "project_id": 8,
+      "created_at": "2016-07-22T08:55:44.161Z",
+      "updated_at": "2016-07-22T08:55:44.161Z",
+      "template": false,
+      "description": "",
+      "type": "ProjectLabel",
+      "priorities": [
+      ]
+    }
+  ],
   "issues": [
     {
       "id": 40,
@@ -28,7 +39,7 @@
       "test_ee_field": "test",
       "milestone": {
         "id": 1,
-        "title": "v0.0",
+        "title": "test milestone",
         "project_id": 8,
         "description": "test milestone",
         "due_date": null,
@@ -55,7 +66,7 @@
         {
           "id": 2,
           "label_id": 2,
-          "target_id": 3,
+          "target_id": 40,
           "target_type": "Issue",
           "created_at": "2016-07-22T08:57:02.840Z",
           "updated_at": "2016-07-22T08:57:02.840Z",
@@ -68,7 +79,37 @@
             "updated_at": "2016-07-22T08:55:44.161Z",
             "template": false,
             "description": "",
-            "priority": null
+            "type": "ProjectLabel"
+          }
+        },
+        {
+          "id": 3,
+          "label_id": 3,
+          "target_id": 40,
+          "target_type": "Issue",
+          "created_at": "2016-07-22T08:57:02.841Z",
+          "updated_at": "2016-07-22T08:57:02.841Z",
+          "label": {
+            "id": 3,
+            "title": "test3",
+            "color": "#428bca",
+            "group_id": 8,
+            "created_at": "2016-07-22T08:55:44.161Z",
+            "updated_at": "2016-07-22T08:55:44.161Z",
+            "template": false,
+            "description": "",
+            "project_id": null,
+            "type": "GroupLabel",
+            "priorities": [
+              {
+                "id": 1,
+                "project_id": 5,
+                "label_id": 1,
+                "priority": 1,
+                "created_at": "2016-10-18T09:35:43.338Z",
+                "updated_at": "2016-10-18T09:35:43.338Z"
+              }
+            ]
           }
         }
       ],
@@ -285,6 +326,31 @@
       "deleted_at": null,
       "due_date": null,
       "moved_to_id": null,
+      "milestone": {
+        "id": 1,
+        "title": "test milestone",
+        "project_id": 8,
+        "description": "test milestone",
+        "due_date": null,
+        "created_at": "2016-06-14T15:02:04.415Z",
+        "updated_at": "2016-06-14T15:02:04.415Z",
+        "state": "active",
+        "iid": 1,
+        "events": [
+          {
+            "id": 487,
+            "target_type": "Milestone",
+            "target_id": 1,
+            "title": null,
+            "data": null,
+            "project_id": 46,
+            "created_at": "2016-06-14T15:02:04.418Z",
+            "updated_at": "2016-06-14T15:02:04.418Z",
+            "action": 1,
+            "author_id": 18
+          }
+        ]
+      },
       "notes": [
         {
           "id": 359,
@@ -498,6 +564,27 @@
       "deleted_at": null,
       "due_date": null,
       "moved_to_id": null,
+      "label_links": [
+        {
+          "id": 99,
+          "label_id": 2,
+          "target_id": 38,
+          "target_type": "Issue",
+          "created_at": "2016-07-22T08:57:02.840Z",
+          "updated_at": "2016-07-22T08:57:02.840Z",
+          "label": {
+            "id": 2,
+            "title": "test2",
+            "color": "#428bca",
+            "project_id": 8,
+            "created_at": "2016-07-22T08:55:44.161Z",
+            "updated_at": "2016-07-22T08:55:44.161Z",
+            "template": false,
+            "description": "",
+            "type": "ProjectLabel"
+          }
+        }
+      ],
       "notes": [
         {
           "id": 367,
@@ -2184,11 +2271,33 @@
         }
       ]
     }
-  ],
-  "labels": [
-
   ],
   "milestones": [
+    {
+      "id": 1,
+      "title": "test milestone",
+      "project_id": 8,
+      "description": "test milestone",
+      "due_date": null,
+      "created_at": "2016-06-14T15:02:04.415Z",
+      "updated_at": "2016-06-14T15:02:04.415Z",
+      "state": "active",
+      "iid": 1,
+      "events": [
+        {
+          "id": 487,
+          "target_type": "Milestone",
+          "target_id": 1,
+          "title": null,
+          "data": null,
+          "project_id": 46,
+          "created_at": "2016-06-14T15:02:04.418Z",
+          "updated_at": "2016-06-14T15:02:04.418Z",
+          "action": 1,
+          "author_id": 18
+        }
+      ]
+    },
     {
       "id": 20,
       "title": "v4.0",
@@ -6482,7 +6591,7 @@
     {
       "id": 37,
       "project_id": 5,
-      "ref": "master",
+      "ref": null,
       "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
       "before_sha": null,
       "push_data": null,
@@ -6876,6 +6985,7 @@
       "note_events": true,
       "build_events": true,
       "category": "issue_tracker",
+      "type": "CustomIssueTrackerService",
       "default": true,
       "wiki_page_events": true
     },
@@ -7305,6 +7415,41 @@
 
   ],
   "protected_branches": [
-
-  ]
+    {
+      "id": 1,
+      "project_id": 9,
+      "name": "master",
+      "created_at": "2016-08-30T07:32:52.426Z",
+      "updated_at": "2016-08-30T07:32:52.426Z",
+      "merge_access_levels": [
+        {
+          "id": 1,
+          "protected_branch_id": 1,
+          "access_level": 40,
+          "created_at": "2016-08-30T07:32:52.458Z",
+          "updated_at": "2016-08-30T07:32:52.458Z"
+        }
+      ],
+      "push_access_levels": [
+        {
+          "id": 1,
+          "protected_branch_id": 1,
+          "access_level": 40,
+          "created_at": "2016-08-30T07:32:52.490Z",
+          "updated_at": "2016-08-30T07:32:52.490Z"
+        }
+      ]
+    }
+  ],
+  "project_feature": {
+    "builds_access_level": 0,
+    "created_at": "2014-12-26T09:26:45.000Z",
+    "id": 2,
+    "issues_access_level": 0,
+    "merge_requests_access_level": 20,
+    "project_id": 4,
+    "snippets_access_level": 20,
+    "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 4d857945fdef667650806af7b437f344637b02c1..3038ab53ad8bf945769ab7559ecafe213dd0de54 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -1,11 +1,12 @@
 require 'spec_helper'
+include ImportExport::CommonUtil
 
 describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
   describe 'restore project tree' do
     let(:user) { create(:user) }
     let(:namespace) { create(:namespace, owner: user) }
     let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
-    let(:project) { create(:empty_project, name: 'project', path: 'project') }
+    let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
     let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
     let(:restored_project_json) { project_tree_restorer.restore }
 
@@ -18,12 +19,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
         expect(restored_project_json).to be true
       end
 
+      it 'restore correct project features' do
+        restored_project_json
+        project = Project.find_by_path('project')
+
+        expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+        expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED)
+        expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED)
+        expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED)
+        expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
+      end
+
+      it 'has the same label associated to two issues' do
+        restored_project_json
+
+        expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
+      end
+
+      it 'has milestones associated to two separate issues' do
+        restored_project_json
+
+        expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
+      end
+
       it 'creates a valid pipeline note' do
         restored_project_json
 
         expect(Ci::Pipeline.first.notes).not_to be_empty
       end
 
+      it 'restores pipelines with missing ref' do
+        restored_project_json
+
+        expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
+      end
+
       it 'restores the correct event with symbolised data' do
         restored_project_json
 
@@ -38,6 +68,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
         expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
       end
 
+      it 'contains the merge access levels on a protected branch' do
+        restored_project_json
+
+        expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
+      end
+
+      it 'contains the push access levels on a protected branch' do
+        restored_project_json
+
+        expect(ProtectedBranch.first.push_access_levels).not_to be_empty
+      end
+
       context 'event at forth level of the tree' do
         let(:event) { Event.where(title: 'test levels').first }
 
@@ -66,10 +108,51 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
         expect(Label.first.label_links.first.target).not_to be_nil
       end
 
-      it 'has milestones associated to issues' do
+      it 'has project labels' do
+        restored_project_json
+
+        expect(ProjectLabel.count).to eq(2)
+      end
+
+      it 'has no group labels' do
+        restored_project_json
+
+        expect(GroupLabel.count).to eq(0)
+      end
+
+      context 'with group' do
+        let!(:project) do 
+          create(:empty_project,
+                                name: 'project',
+                                path: 'project',
+                                builds_access_level: ProjectFeature::DISABLED,
+                                issues_access_level: ProjectFeature::DISABLED,
+                                group: create(:group)) 
+        end
+
+        it 'has group labels' do
+          restored_project_json
+
+          expect(GroupLabel.count).to eq(1)
+        end
+
+        it 'has label priorities' do
+          restored_project_json
+
+          expect(GroupLabel.first.priorities).not_to be_empty
+        end
+      end
+
+      it 'has a project feature' do
         restored_project_json
 
-        expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
+        expect(project.project_feature).not_to be_nil
+      end
+
+      it 'restores the correct service' do
+        restored_project_json
+
+        expect(CustomIssueTrackerService.first).not_to be_nil
       end
 
       context 'Merge requests' do
@@ -93,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
           expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
         end
       end
+
+      context 'project.json file access check' do
+        it 'does not read a symlink' do
+          Dir.mktmpdir do |tmpdir|
+            setup_symlink(tmpdir, 'project.json')
+            allow(shared).to receive(:export_path).and_call_original
+
+            restored_project_json
+
+            expect(shared.errors.first).not_to include('test')
+          end
+        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 3a86a4ce07c826aea84bb60cefca3a5c3552a78f..c8bba553558e50d656c97770d47ab194bd850a76 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -111,6 +111,30 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
         expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
       end
 
+      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'])
+      end
+
+      it 'has priorities associated to labels' do
+        priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+
+        expect(priorities.flatten).not_to be_empty
+      end
+
+      it 'saves the correct service type' do
+        expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
+      end
+
+      it 'has project feature' do
+        project_feature = saved_project_json['project_feature']
+        expect(project_feature).not_to be_empty
+        expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED)
+        expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED)
+        expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
+      end
+
       it 'does not complain about non UTF-8 characters in MR diffs' do
         ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n    LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n    KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n    YXR'")
 
@@ -123,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
     issue = create(:issue, assignee: user)
     snippet = create(:project_snippet)
     release = create(:release)
+    group = create(:group)
 
     project = create(:project,
                      :public,
                      issues: [issue],
                      snippets: [snippet],
-                     releases: [release]
+                     releases: [release],
+                     group: group
                     )
-    label = create(:label, project: project)
-    create(:label_link, label: label, target: issue)
+    project_label = create(:label, project: project)
+    group_label = create(:group_label, group: group)
+    create(:label_link, label: project_label, target: issue)
+    create(:label_link, label: group_label, target: issue)
+    create(:label_priority, label: group_label, priority: 1)
     milestone = create(:milestone, project: project)
     merge_request = create(:merge_request, source_project: project, milestone: milestone)
     commit_status = create(:commit_status, project: project)
@@ -153,6 +182,11 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
            commit_id: ci_pipeline.sha)
 
     create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+    create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
+
+    project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+    project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
+    project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
 
     project
   end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b6dec41d218f095fc04cb4daf779c45cc7dd89b1..3ceb1e7e803216aab16c23222e6084b8ee8e9e14 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true  do
       expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
     end
 
+    it 'generates the correct hash for a single project feature relation' do
+      setup_yaml(project_tree: [:project_feature])
+
+      expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature])
+    end
+
     it 'generates the correct hash for a multiple project relation' do
       setup_yaml(project_tree: [:issues, :snippets])
 
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3aa492a8ab14d7ae54df04a6e8b0a69f17a1f048
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationFactory, lib: true do
+  let(:project) { create(:empty_project) }
+  let(:members_mapper) { double('members_mapper').as_null_object }
+  let(:user) { create(:user) }
+  let(:created_object) do
+    described_class.create(relation_sym: relation_sym,
+                           relation_hash: relation_hash,
+                           members_mapper: members_mapper,
+                           user: user,
+                           project_id: project.id)
+  end
+
+  context 'hook object' do
+    let(:relation_sym) { :hooks }
+    let(:id) { 999 }
+    let(:service_id) { 99 }
+    let(:original_project_id) { 8 }
+    let(:token) { 'secret' }
+
+    let(:relation_hash) do
+      {
+        'id' => id,
+        'url' => 'https://example.json',
+        'project_id' => original_project_id,
+        'created_at' => '2016-08-12T09:41:03.462Z',
+        'updated_at' => '2016-08-12T09:41:03.462Z',
+        'service_id' => service_id,
+        'push_events' => true,
+        'issues_events' => false,
+        'merge_requests_events' => true,
+        'tag_push_events' => false,
+        'note_events' => true,
+        'enable_ssl_verification' => true,
+        'build_events' => false,
+        'wiki_page_events' => true,
+        'token' => token
+      }
+    end
+
+    it 'does not have the original ID' do
+      expect(created_object.id).not_to eq(id)
+    end
+
+    it 'does not have the original service_id' do
+      expect(created_object.service_id).not_to eq(service_id)
+    end
+
+    it 'does not have the original project_id' do
+      expect(created_object.project_id).not_to eq(original_project_id)
+    end
+
+    it 'has the new project_id' do
+      expect(created_object.project_id).to eq(project.id)
+    end
+
+    it 'has a token' do
+      expect(created_object.token).to eq(token)
+    end
+
+    context 'original service exists' do
+      let(:service_id) { Service.create(project: project).id }
+
+      it 'does not have the original service_id' do
+        expect(created_object.service_id).not_to eq(service_id)
+      end
+    end
+  end
+
+  # Mocks an ActiveRecordish object with the dodgy columns
+  class FooModel
+    include ActiveModel::Model
+
+    def initialize(params)
+      params.each { |key, value| send("#{key}=", value) }
+    end
+
+    def values
+      instance_variables.map { |ivar| instance_variable_get(ivar) }
+    end
+  end
+
+  # `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
+  # re-assigned by described_class.
+  context 'Potentially hazardous foreign keys' do
+    let(:relation_sym) { :hazardous_foo_model }
+    let(:relation_hash) do
+      {
+        'service_id' => 99,
+        'moved_to_id' => 99,
+        'namespace_id' => 99,
+        'ci_id' => 99,
+        'random_project_id' => 99,
+        'random_id' => 99,
+        'milestone_id' => 99,
+        'project_id' => 99,
+        'user_id' => 99,
+      }
+    end
+
+    class HazardousFooModel < FooModel
+      attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
+    end
+
+    it 'does not preserve any foreign key IDs' do
+      expect(created_object.values).not_to include(99)
+    end
+  end
+
+  context 'Project references' do
+    let(:relation_sym) { :project_foo_model }
+    let(:relation_hash) do
+      Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
+    end
+
+    class ProjectFooModel < FooModel
+      attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
+    end
+
+    it 'does not preserve any project foreign key IDs' do
+      expect(created_object.values).not_to include(99)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..07a2c3168995d32363f9f698a7ac1764bec9e82b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -0,0 +1,342 @@
+---
+Issue:
+- id
+- title
+- assignee_id
+- author_id
+- project_id
+- created_at
+- updated_at
+- position
+- branch_name
+- description
+- state
+- iid
+- updated_by_id
+- confidential
+- deleted_at
+- due_date
+- moved_to_id
+- lock_version
+- milestone_id
+- weight
+Event:
+- id
+- target_type
+- target_id
+- title
+- data
+- project_id
+- created_at
+- updated_at
+- action
+- author_id
+Note:
+- id
+- note
+- noteable_type
+- author_id
+- created_at
+- updated_at
+- project_id
+- attachment
+- line_code
+- commit_id
+- noteable_id
+- system
+- st_diff
+- updated_by_id
+- type
+- position
+- original_position
+- resolved_at
+- resolved_by_id
+- discussion_id
+- original_discussion_id
+LabelLink:
+- id
+- label_id
+- target_id
+- target_type
+- created_at
+- updated_at
+ProjectLabel:
+- id
+- title
+- color
+- group_id
+- project_id
+- type
+- created_at
+- updated_at
+- template
+- description
+- priority
+Milestone:
+- id
+- title
+- project_id
+- description
+- due_date
+- created_at
+- updated_at
+- state
+- iid
+ProjectSnippet:
+- id
+- title
+- content
+- author_id
+- project_id
+- created_at
+- updated_at
+- file_name
+- type
+- visibility_level
+Release:
+- id
+- tag
+- description
+- project_id
+- created_at
+- updated_at
+ProjectMember:
+- id
+- access_level
+- source_id
+- source_type
+- user_id
+- notification_level
+- type
+- created_at
+- updated_at
+- created_by_id
+- invite_email
+- invite_token
+- invite_accepted_at
+- requested_at
+- expires_at
+User:
+- id
+- username
+- email
+MergeRequest:
+- id
+- target_branch
+- source_branch
+- source_project_id
+- author_id
+- assignee_id
+- title
+- created_at
+- updated_at
+- state
+- merge_status
+- target_project_id
+- iid
+- description
+- position
+- locked_at
+- updated_by_id
+- merge_error
+- merge_params
+- merge_when_build_succeeds
+- merge_user_id
+- merge_commit_sha
+- deleted_at
+- in_progress_merge_commit_sha
+- lock_version
+- milestone_id
+- approvals_before_merge
+- rebase_commit_sha
+MergeRequestDiff:
+- id
+- state
+- st_commits
+- merge_request_id
+- created_at
+- updated_at
+- base_commit_sha
+- real_size
+- head_commit_sha
+- start_commit_sha
+Ci::Pipeline:
+- id
+- project_id
+- ref
+- sha
+- before_sha
+- push_data
+- created_at
+- updated_at
+- tag
+- yaml_errors
+- committed_at
+- gl_project_id
+- status
+- started_at
+- finished_at
+- duration
+- user_id
+- lock_version
+CommitStatus:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- gl_project_id
+- artifacts_metadata
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+- lock_version
+Ci::Variable:
+- id
+- project_id
+- key
+- value
+- encrypted_value
+- encrypted_value_salt
+- encrypted_value_iv
+- gl_project_id
+Ci::Trigger:
+- id
+- token
+- project_id
+- deleted_at
+- created_at
+- updated_at
+- gl_project_id
+DeployKey:
+- id
+- user_id
+- created_at
+- updated_at
+- key
+- title
+- type
+- fingerprint
+- public
+Service:
+- id
+- type
+- title
+- project_id
+- created_at
+- updated_at
+- active
+- properties
+- template
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- build_events
+- category
+- default
+- wiki_page_events
+- confidential_issues_events
+ProjectHook:
+- id
+- url
+- project_id
+- created_at
+- updated_at
+- type
+- service_id
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- enable_ssl_verification
+- build_events
+- wiki_page_events
+- token
+- group_id
+- confidential_issues_events
+ProtectedBranch:
+- id
+- project_id
+- name
+- created_at
+- updated_at
+Project:
+- description
+- issues_enabled
+- merge_requests_enabled
+- wiki_enabled
+- snippets_enabled
+- visibility_level
+- archived
+Author:
+- name
+ProjectFeature:
+- id
+- project_id
+- merge_requests_access_level
+- issues_access_level
+- wiki_access_level
+- snippets_access_level
+- builds_access_level
+- repository_access_level
+- created_at
+- updated_at
+ProtectedBranch::MergeAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+ProtectedBranch::PushAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+AwardEmoji:
+- id
+- user_id
+- name
+- awardable_type
+- created_at
+- updated_at
+LabelPriority:
+- id
+- project_id
+- label_id
+- priority
+- created_at
+- updated_at
\ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index 90c6d1c67f6ec920cc4e2feafeee930027aa8f2d..2405ac5abfe3ea5613f6012a191436b96c2d47f3 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -1,8 +1,10 @@
 require 'spec_helper'
+include ImportExport::CommonUtil
 
 describe Gitlab::ImportExport::VersionChecker, services: true do
+  let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+
   describe 'bundle a project Git repo' do
-    let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
     let(:version) { Gitlab::ImportExport.version }
 
     before do
@@ -23,7 +25,19 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
       it 'shows the correct error message' do
         described_class.check!(shared: shared)
 
-        expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+        expect(shared.errors.first).to eq("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
+      end
+    end
+  end
+
+  describe 'version file access check' do
+    it 'does not read a symlink' do
+      Dir.mktmpdir do |tmpdir|
+        setup_symlink(tmpdir, 'VERSION')
+
+        described_class.check!(shared: shared)
+
+        expect(shared.errors.first).not_to include('test')
       end
     end
   end
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index 4847b5f3b0e62e724e36459ee28ffd7c04f909e0..563c074017a5a72676c214faf7188dbc39afb882 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -1,24 +1,105 @@
 require 'spec_helper'
 
 describe Gitlab::LDAP::Adapter, lib: true do
-  let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' }
+  include LdapHelpers
+
+  let(:ldap) { double(:ldap) }
+  let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+  describe '#users' do
+    before do
+      stub_ldap_config(base: 'dc=example,dc=com')
+    end
+
+    it 'searches with the proper options when searching by uid' do
+      # Requires this expectation style to match the filter
+      expect(adapter).to receive(:ldap_search) do |arg|
+        expect(arg[:filter].to_s).to eq('(uid=johndoe)')
+        expect(arg[:base]).to eq('dc=example,dc=com')
+        expect(arg[:attributes]).to match(%w{uid cn mail dn})
+      end.and_return({})
+
+      adapter.users('uid', 'johndoe')
+    end
+
+    it 'searches with the proper options when searching by dn' do
+      expect(adapter).to receive(:ldap_search).with(
+        base: 'uid=johndoe,ou=users,dc=example,dc=com',
+        scope: Net::LDAP::SearchScope_BaseObject,
+        attributes: %w{uid cn mail dn},
+        filter: nil
+      ).and_return({})
+
+      adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com')
+    end
+
+    it 'searches with the proper options when searching with a limit' do
+      expect(adapter)
+        .to receive(:ldap_search).with(hash_including(size: 100)).and_return({})
+
+      adapter.users('uid', 'johndoe', 100)
+    end
+
+    it 'returns an LDAP::Person if search returns a result' do
+      entry = ldap_user_entry('johndoe')
+      allow(adapter).to receive(:ldap_search).and_return([entry])
+
+      results = adapter.users('uid', 'johndoe')
+
+      expect(results.size).to eq(1)
+      expect(results.first.uid).to eq('johndoe')
+    end
+
+    it 'returns empty array if search entry does not respond to uid' do
+      entry = Net::LDAP::Entry.new
+      entry['dn'] = user_dn('johndoe')
+      allow(adapter).to receive(:ldap_search).and_return([entry])
+
+      results = adapter.users('uid', 'johndoe')
+
+      expect(results).to be_empty
+    end
+
+    it 'uses the right uid attribute when non-default' do
+      stub_ldap_config(uid: 'sAMAccountName')
+      expect(adapter).to receive(:ldap_search).with(
+        hash_including(attributes: %w{sAMAccountName cn mail dn})
+      ).and_return({})
+
+      adapter.users('sAMAccountName', 'johndoe')
+    end
+  end
 
   describe '#dn_matches_filter?' do
-    let(:ldap) { double(:ldap) }
     subject { adapter.dn_matches_filter?(:dn, :filter) }
-    before { allow(adapter).to receive(:ldap).and_return(ldap) }
+
+    context "when the search result is non-empty" do
+      before { allow(adapter).to receive(:ldap_search).and_return([:foo]) }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context "when the search result is empty" do
+      before { allow(adapter).to receive(:ldap_search).and_return([]) }
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '#ldap_search' do
+    subject { adapter.ldap_search(base: :dn, filter: :filter) }
 
     context "when the search is successful" do
       context "and the result is non-empty" do
         before { allow(ldap).to receive(:search).and_return([:foo]) }
 
-        it { is_expected.to be_truthy }
+        it { is_expected.to eq [:foo] }
       end
 
       context "and the result is empty" do
         before { allow(ldap).to receive(:search).and_return([]) }
 
-        it { is_expected.to be_falsey }
+        it { is_expected.to eq [] }
       end
     end
 
@@ -30,7 +111,22 @@ describe Gitlab::LDAP::Adapter, lib: true do
         )
       end
 
-      it { is_expected.to be_falsey }
+      it { is_expected.to eq [] }
+    end
+
+    context "when the search raises an LDAP exception" do
+      before do
+        allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" }
+        allow(Rails.logger).to receive(:warn)
+      end
+
+      it { is_expected.to eq [] }
+
+      it 'logs the error' do
+        subject
+        expect(Rails.logger).to have_received(:warn).with(
+          "LDAP search raised exception Net::LDAP::Error: some error")
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 835853a83a4357c30aaebf88027df0b936bf8925..f5ebe70308364fc1ce1e971497a1bac828218af9 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -1,20 +1,51 @@
 require 'spec_helper'
 
 describe Gitlab::LDAP::Config, lib: true do
-  let(:config) { Gitlab::LDAP::Config.new provider }
-  let(:provider) { 'ldapmain' }
+  include LdapHelpers
+
+  let(:config) { Gitlab::LDAP::Config.new('ldapmain') }
 
   describe '#initalize' do
     it 'requires a provider' do
       expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError
     end
 
-    it "works" do
+    it 'works' do
       expect(config).to be_a described_class
     end
 
-    it "raises an error if a unknow provider is used" do
+    it 'raises an error if a unknown provider is used' do
       expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError)
     end
   end
+
+  describe '#has_auth?' do
+    it 'is true when password is set' do
+      stub_ldap_config(
+        options: {
+          'bind_dn'  => 'uid=admin,dc=example,dc=com',
+          'password' => 'super_secret'
+        }
+      )
+
+      expect(config.has_auth?).to be_truthy
+    end
+
+    it 'is true when bind_dn is set and password is empty' do
+      stub_ldap_config(
+        options: {
+          'bind_dn'  => 'uid=admin,dc=example,dc=com',
+          'password' => ''
+        }
+      )
+
+      expect(config.has_auth?).to be_truthy
+    end
+
+    it 'is false when password and bind_dn are not set' do
+      stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil })
+
+      expect(config.has_auth?).to be_falsey
+    end
+  end
 end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9c1163e22abd023159d79f6af6f306e2fe86da4
--- /dev/null
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::LfsToken, lib: true do
+  describe '#token' do
+    shared_examples 'an LFS token generator' do
+      it 'returns a randomly generated token' do
+        token = handler.token
+
+        expect(token).not_to be_nil
+        expect(token).to be_a String
+        expect(token.length).to eq 50
+      end
+
+      it 'returns the correct token based on the key' do
+        token = handler.token
+
+        expect(handler.token).to eq(token)
+      end
+    end
+
+    context 'when the actor is a user' do
+      let(:actor) { create(:user) }
+      let(:handler) { described_class.new(actor) }
+
+      it_behaves_like 'an LFS token generator'
+
+      it 'returns the correct username' do
+        expect(handler.actor_name).to eq(actor.username)
+      end
+
+      it 'returns the correct token type' do
+        expect(handler.type).to eq(:lfs_token)
+      end
+    end
+
+    context 'when the actor is a deploy key' do
+      let(:actor) { create(:deploy_key) }
+      let(:handler) { described_class.new(actor) }
+
+      it_behaves_like 'an LFS token generator'
+
+      it 'returns the correct username' do
+        expect(handler.actor_name).to eq("lfs+deploy-key-#{actor.id}")
+      end
+
+      it 'returns the correct token type' do
+        expect(handler.type).to eq(:lfs_deploy_token)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f718d536130a35e09f2948129461067c1f9b570c..f26fca52c5067a77d0d7679fc6fabc802ee462a0 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -23,6 +23,24 @@ describe Gitlab::Metrics::Metric do
     it { is_expected.to eq({ host: 'localtoast' }) }
   end
 
+  describe '#type' do
+    subject { metric.type }
+
+    it { is_expected.to eq(:metric) }
+  end
+
+  describe '#event?' do
+    it 'returns false for a regular metric' do
+      expect(metric.event?).to eq(false)
+    end
+
+    it 'returns true for an event metric' do
+      expect(metric).to receive(:type).and_return(:event)
+
+      expect(metric.event?).to eq(true)
+    end
+  end
+
   describe '#to_hash' do
     it 'returns a Hash' do
       expect(metric.to_hash).to be_an_instance_of(Hash)
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index f264ed64029a532763611948e0f0b03f7e3e7392..bcaffd279090309ca3a141327e3f5bade87af6ce 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do
     end
 
     it 'tags a transaction with the name and action of a controller' do
-      klass      = double(:klass, name: 'TestController')
+      klass      = double(:klass, name: 'TestController', content_type: 'text/html')
       controller = double(:controller, class: klass, action_name: 'show')
 
       env['action_controller.instance'] = controller
@@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
       middleware.call(env)
     end
 
-    it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+    it 'tags a transaction with the method and path of the route in the grape endpoint' do
       route    = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
       endpoint = double(:endpoint, route: route)
 
@@ -45,6 +45,15 @@ describe Gitlab::Metrics::RackMiddleware do
 
       middleware.call(env)
     end
+
+    it 'tracks any raised exceptions' do
+      expect(app).to receive(:call).with(env).and_raise(RuntimeError)
+
+      expect_any_instance_of(Gitlab::Metrics::Transaction).
+        to receive(:add_event).with(:rails_exception)
+
+      expect { middleware.call(env) }.to raise_error(RuntimeError)
+    end
   end
 
   describe '#transaction_from_env' do
@@ -78,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do
 
   describe '#tag_controller' do
     let(:transaction) { middleware.transaction_from_env(env) }
+    let(:content_type) { 'text/html' }
 
-    it 'tags a transaction with the name and action of a controller' do
+    before do
       klass      = double(:klass, name: 'TestController')
-      controller = double(:controller, class: klass, action_name: 'show')
+      controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
 
       env['action_controller.instance'] = controller
+    end
 
+    it 'tags a transaction with the name and action of a controller' do
       middleware.tag_controller(transaction, env)
 
       expect(transaction.action).to eq('TestController#show')
     end
+
+    context 'when the response content type is not :html' do
+      let(:content_type) { 'application/json' }
+
+      it 'appends the mime type to the transaction action' do
+        middleware.tag_controller(transaction, env)
+
+        expect(transaction.action).to eq('TestController#show.json')
+      end
+    end
   end
 
   describe '#tag_endpoint' do
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 4d2aa03e722cbac5d5d2c8eef9cc39980ccf28a1..acaba785606e13e9b43348a3fa4247eb3ac56d91 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -12,7 +12,9 @@ describe Gitlab::Metrics::SidekiqMiddleware do
         with('TestWorker#perform').
         and_call_original
 
-      expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+      expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+        with(:sidekiq_queue_duration, instance_of(Float))
+
       expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
 
       middleware.call(worker, message, :test) { nil }
@@ -25,10 +27,28 @@ describe Gitlab::Metrics::SidekiqMiddleware do
         with('TestWorker#perform').
         and_call_original
 
-      expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+      expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+        with(:sidekiq_queue_duration, instance_of(Float))
+
       expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
 
       middleware.call(worker, {}, :test) { nil }
     end
+
+    it 'tracks any raised exceptions' do
+      worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+      expect_any_instance_of(Gitlab::Metrics::Transaction).
+        to receive(:run).and_raise(RuntimeError)
+
+      expect_any_instance_of(Gitlab::Metrics::Transaction).
+        to receive(:add_event).with(:sidekiq_exception)
+
+      expect_any_instance_of(Gitlab::Metrics::Transaction).
+        to receive(:finish)
+
+      expect { middleware.call(worker, message, :test) }.
+        to raise_error(RuntimeError)
+    end
   end
 end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index f1a191d94100889466524e3a8a81e9e052a6e590..3887c04c83214b19f531523afd6e646e0c3104f1 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -142,5 +142,62 @@ describe Gitlab::Metrics::Transaction do
 
       transaction.submit
     end
+
+    it 'does not add an action tag for events' do
+      transaction.action = 'Foo#bar'
+      transaction.add_event(:meow)
+
+      hash = {
+        series:    'events',
+        tags:      { event: :meow },
+        values:    { count: 1 },
+        timestamp: an_instance_of(Fixnum)
+      }
+
+      expect(Gitlab::Metrics).to receive(:submit_metrics).
+        with([hash])
+
+      transaction.submit
+    end
+  end
+
+  describe '#add_event' do
+    it 'adds a metric' do
+      transaction.add_event(:meow)
+
+      expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
+    end
+
+    it "does not prefix the metric's series name" do
+      transaction.add_event(:meow)
+
+      metric = transaction.metrics[0]
+
+      expect(metric.series).to eq(described_class::EVENT_SERIES)
+    end
+
+    it 'tracks a counter for every event' do
+      transaction.add_event(:meow)
+
+      metric = transaction.metrics[0]
+
+      expect(metric.values).to eq(count: 1)
+    end
+
+    it 'tracks the event name' do
+      transaction.add_event(:meow)
+
+      metric = transaction.metrics[0]
+
+      expect(metric.tags).to eq(event: :meow)
+    end
+
+    it 'allows tracking of custom tags' do
+      transaction.add_event(:meow, animal: 'cat')
+
+      metric = transaction.metrics[0]
+
+      expect(metric.tags).to eq(event: :meow, animal: 'cat')
+    end
   end
 end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 84f9475a0f85fbe66d58c8adce93d0eb7edd04f7..ab6e311b1e80b4034febc97db91c69f4393a8bfd 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -153,4 +153,28 @@ describe Gitlab::Metrics do
       expect(described_class.series_prefix).to be_an_instance_of(String)
     end
   end
+
+  describe '.add_event' do
+    context 'without a transaction' do
+      it 'does nothing' do
+        expect_any_instance_of(Gitlab::Metrics::Transaction).
+          not_to receive(:add_event)
+
+        Gitlab::Metrics.add_event(:meow)
+      end
+    end
+
+    context 'with a transaction' do
+      it 'adds an event' do
+        transaction = Gitlab::Metrics::Transaction.new
+
+        expect(transaction).to receive(:add_event).with(:meow)
+
+        expect(Gitlab::Metrics).to receive(:current_transaction).
+          and_return(transaction)
+
+        Gitlab::Metrics.add_event(:meow)
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index fd6f684db0c17bf1244fd9a4573ce4e8231bb045..168090d5b5c3d34d3daa4dc3f2c295874505753e 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
       end
 
       it 'sets proxy_flight_time and calls the app when the header is present' do
-        env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+        env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '123'
         expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
         expect(middleware.call(env)).to eq('yay')
       end
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..498dc514c8c2b45caf276bd952ddcc846faebe17
--- /dev/null
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -0,0 +1,39 @@
+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) }
+
+    it 'does not reload object if state changes' do
+      expect(pipeline).not_to receive(:reload)
+      expect(pipeline).to receive(:succeed).and_call_original
+
+      described_class.retry_lock(pipeline) do |subject|
+        subject.succeed
+      end
+    end
+
+    it 'retries action if exception is raised' do
+      pipeline.succeed
+
+      expect(pipeline2).to receive(:reload).and_call_original
+      expect(pipeline2).to receive(:drop).twice.and_call_original
+
+      described_class.retry_lock(pipeline2) do |subject|
+        subject.drop
+      end
+    end
+
+    it 'raises exception when too many retries' do
+      expect(pipeline).to receive(:drop).twice.and_call_original
+
+      expect do
+        described_class.retry_lock(pipeline, 1) do |subject|
+          subject.lock_version = 100
+          subject.drop
+        end
+      end.to raise_error(ActiveRecord::StaleObjectError)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index e8b236426e907248b41d4c25d52662f7ecf01355..4ae216d55b0b3bcfc7232b11f348d3628c99580f 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -40,4 +40,13 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
     it { expect(@status).to be_zero }
     it { expect(@output).to include('spec') }
   end
+
+  context 'use stdin' do
+    before do
+      @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' }
+    end
+  
+    it { expect(@status).to be_zero }
+    it { expect(@output).to eq('hello') }
+  end
 end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index e54f5ffb3124930a2d2aa9e4e4715ece5676dffc..e5406fb2d33de0f9a28de0a6f14402f6e848958b 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,19 +3,27 @@ require 'spec_helper'
 describe Gitlab::Redis do
   let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
 
-  before(:each) { described_class.reset_params! }
-  after(:each) { described_class.reset_params! }
+  before(:each) { clear_raw_config }
+  after(:each) { clear_raw_config }
 
   describe '.params' do
     subject { described_class.params }
 
+    it 'withstands mutation' do
+      params1 = described_class.params
+      params2 = described_class.params
+      params1[:foo] = :bar
+
+      expect(params2).not_to have_key(:foo)
+    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 }
 
       context 'with old format' do
         it 'returns path key instead' do
-          expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+          stub_const("#{described_class}::CONFIG_FILE", config_old)
 
           is_expected.to include(path: '/path/to/old/redis.sock')
           is_expected.not_to have_key(:url)
@@ -24,7 +32,7 @@ describe Gitlab::Redis do
 
       context 'with new format' do
         it 'returns path key instead' do
-          expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+          stub_const("#{described_class}::CONFIG_FILE", config_new)
 
           is_expected.to include(path: '/path/to/redis.sock')
           is_expected.not_to have_key(:url)
@@ -38,7 +46,7 @@ describe Gitlab::Redis do
 
       context 'with old format' do
         it 'returns hash with host, port, db, and password' do
-          expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+          stub_const("#{described_class}::CONFIG_FILE", config_old)
 
           is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
           is_expected.not_to have_key(:url)
@@ -47,7 +55,7 @@ describe Gitlab::Redis do
 
       context 'with new format' do
         it 'returns hash with host, port, db, and password' do
-          expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+          stub_const("#{described_class}::CONFIG_FILE", config_new)
 
           is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
           is_expected.not_to have_key(:url)
@@ -56,6 +64,107 @@ describe Gitlab::Redis do
     end
   end
 
+  describe '.url' do
+    it 'withstands mutation' do
+      url1 = described_class.url
+      url2 = described_class.url
+      url1 << 'foobar'
+
+      expect(url2).not_to end_with('foobar')
+    end
+  end
+
+  describe '._raw_config' do
+    subject { described_class._raw_config }
+
+    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
+
+  describe '.with' do
+    before { clear_pool }
+    after { clear_pool }
+
+    context 'when running not on sidekiq workers' do
+      before { allow(Sidekiq).to receive(:server?).and_return(false) }
+
+      it 'instantiates a connection pool with size 5' do
+        expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+
+        described_class.with { |_redis| true }
+      end
+    end
+
+    context 'when running on sidekiq workers' do
+      before do
+        allow(Sidekiq).to receive(:server?).and_return(true)
+        allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 })
+      end
+
+      it 'instantiates a connection pool with a size based on the concurrency of the worker' do
+        expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+
+        described_class.with { |_redis| true }
+      end
+    end
+  end
+
+  describe '#sentinels' 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') }
+
+      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') }
+
+      it 'returns nil' do
+        stub_const("#{described_class}::CONFIG_FILE", config)
+
+        is_expected.to be_nil
+      end
+    end
+  end
+
+  describe '#sentinels?' 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') }
+
+      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') }
+
+      it 'returns false' do
+        stub_const("#{described_class}::CONFIG_FILE", config)
+
+        is_expected.to be_falsey
+      end
+    end
+  end
+
   describe '#raw_config_hash' do
     it 'returns default redis url when no config file is present' do
       expect(subject).to receive(:fetch_config) { false }
@@ -71,9 +180,21 @@ describe Gitlab::Redis do
 
   describe '#fetch_config' do
     it 'returns false when no config file is present' do
-      allow(File).to receive(:exist?).with(redis_config) { false }
+      allow(described_class).to receive(:_raw_config) { false }
 
       expect(subject.send(:fetch_config)).to be_falsey
     end
   end
+
+  def clear_raw_config
+    described_class.remove_instance_variable(:@_raw_config)
+  rescue NameError
+    # raised if @_raw_config was not set; ignore
+  end
+
+  def clear_pool
+    described_class.remove_instance_variable(:@pool)
+  rescue NameError
+    # raised if @pool was not set; ignore
+  end
 end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7b4ccc83915a79ce873cf07a767cae5bc0145554..bf0ab9635fd308d49f29f17b6cc0d9d361eb5bf2 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
 describe Gitlab::ReferenceExtractor, lib: true do
   let(:project) { create(:project) }
 
+  before { project.team << [project.creator, :developer] }
+
   subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
 
   it 'accesses valid user objects' do
@@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do
   end
 
   it 'accesses valid issue objects' do
-    project.team << [project.creator, :developer]
     @i0 = create(:issue, project: project)
     @i1 = create(:issue, project: project)
 
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 8a656ab0ee9e1d45dfe19445d74367874e029fa1..dfbefad6367ceb7c52fe85a5a49f01618fa8bfa5 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -12,12 +12,6 @@ describe Gitlab::SearchResults do
   let!(:milestone) { create(:milestone, project: project, title: 'foo') }
   let(:results) { described_class.new(user, Project.all, 'foo') }
 
-  describe '#total_count' do
-    it 'returns the total amount of search hits' do
-      expect(results.total_count).to eq(4)
-    end
-  end
-
   describe '#projects_count' do
     it 'returns the total amount of projects' do
       expect(results.projects_count).to eq(1)
@@ -42,18 +36,6 @@ describe Gitlab::SearchResults do
     end
   end
 
-  describe '#empty?' do
-    it 'returns true when there are no search results' do
-      allow(results).to receive(:total_count).and_return(0)
-
-      expect(results.empty?).to eq(true)
-    end
-
-    it 'returns false when there are search results' do
-      expect(results.empty?).to eq(false)
-    end
-  end
-
   describe 'confidential issues' do
     let(:project_1) { create(:empty_project) }
     let(:project_2) { create(:empty_project) }
diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c9c2f314e576df9f2e0359fd7e8022882302f97c
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::CommandDefinition do
+  subject { described_class.new(:command) }
+
+  describe "#all_names" do
+    context "when the command has aliases" do
+      before do
+        subject.aliases = [:alias1, :alias2]
+      end
+
+      it "returns an array with the name and aliases" do
+        expect(subject.all_names).to eq([:command, :alias1, :alias2])
+      end
+    end
+
+    context "when the command doesn't have aliases" do
+      it "returns an array with the name" do
+        expect(subject.all_names).to eq([:command])
+      end
+    end
+  end
+
+  describe "#noop?" do
+    context "when the command has an action block" do
+      before do
+        subject.action_block = proc { }
+      end
+
+      it "returns false" do
+        expect(subject.noop?).to be false
+      end
+    end
+
+    context "when the command doesn't have an action block" do
+      it "returns true" do
+        expect(subject.noop?).to be true
+      end
+    end
+  end
+
+  describe "#available?" do
+    let(:opts) { { go: false } }
+
+    context "when the command has a condition block" do
+      before do
+        subject.condition_block = proc { go }
+      end
+
+      context "when the condition block returns true" do
+        before do
+          opts[:go] = true
+        end
+
+        it "returns true" do
+          expect(subject.available?(opts)).to be true
+        end
+      end
+
+      context "when the condition block returns false" do
+        it "returns false" do
+          expect(subject.available?(opts)).to be false
+        end
+      end
+    end
+
+    context "when the command doesn't have a condition block" do
+      it "returns true" do
+        expect(subject.available?(opts)).to be true
+      end
+    end
+  end
+
+  describe "#execute" do
+    let(:context) { OpenStruct.new(run: false) }
+
+    context "when the command is a noop" do
+      it "doesn't execute the command" do
+        expect(context).not_to receive(:instance_exec)
+
+        subject.execute(context, {}, nil)
+
+        expect(context.run).to be false
+      end
+    end
+
+    context "when the command is not a noop" do
+      before do
+        subject.action_block = proc { self.run = true }
+      end
+
+      context "when the command is not available" do
+        before do
+          subject.condition_block = proc { false }
+        end
+
+        it "doesn't execute the command" do
+          subject.execute(context, {}, nil)
+
+          expect(context.run).to be false
+        end
+      end
+
+      context "when the command is available" do
+        context "when the commnd has no arguments" do
+          before do
+            subject.action_block = proc { self.run = true }
+          end
+
+          context "when the command is provided an argument" do
+            it "executes the command" do
+              subject.execute(context, {}, true)
+
+              expect(context.run).to be true
+            end
+          end
+
+          context "when the command is not provided an argument" do
+            it "executes the command" do
+              subject.execute(context, {}, nil)
+
+              expect(context.run).to be true
+            end
+          end
+        end
+
+        context "when the command has 1 required argument" do
+          before do
+            subject.action_block = ->(arg) { self.run = arg }
+          end
+
+          context "when the command is provided an argument" do
+            it "executes the command" do
+              subject.execute(context, {}, true)
+
+              expect(context.run).to be true
+            end
+          end
+
+          context "when the command is not provided an argument" do
+            it "doesn't execute the command" do
+              subject.execute(context, {}, nil)
+
+              expect(context.run).to be false
+            end
+          end
+        end
+
+        context "when the command has 1 optional argument" do
+          before do
+            subject.action_block = proc { |arg = nil| self.run = arg || true }
+          end
+
+          context "when the command is provided an argument" do
+            it "executes the command" do
+              subject.execute(context, {}, true)
+
+              expect(context.run).to be true
+            end
+          end
+
+          context "when the command is not provided an argument" do
+            it "executes the command" do
+              subject.execute(context, {}, nil)
+
+              expect(context.run).to be true
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..26217a0e3b2c6d29ece057cc64e5b8166553eb4d
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Dsl do
+  before :all do
+    DummyClass = Struct.new(:project) do
+      include Gitlab::SlashCommands::Dsl
+
+      desc 'A command with no args'
+      command :no_args, :none do
+        "Hello World!"
+      end
+
+      params 'The first argument'
+      command :one_arg, :once, :first do |arg1|
+        arg1
+      end
+
+      desc do
+        "A dynamic description for #{noteable.upcase}"
+      end
+      params 'The first argument', 'The second argument'
+      command :two_args do |arg1, arg2|
+        [arg1, arg2]
+      end
+
+      command :cc
+
+      condition do
+        project == 'foo'
+      end
+      command :cond_action do |arg|
+        arg
+      end
+    end
+  end
+
+  describe '.command_definitions' do
+    it 'returns an array with commands definitions' do
+      no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions
+
+      expect(no_args_def.name).to eq(:no_args)
+      expect(no_args_def.aliases).to eq([:none])
+      expect(no_args_def.description).to eq('A command with no args')
+      expect(no_args_def.params).to eq([])
+      expect(no_args_def.condition_block).to be_nil
+      expect(no_args_def.action_block).to be_a_kind_of(Proc)
+
+      expect(one_arg_def.name).to eq(:one_arg)
+      expect(one_arg_def.aliases).to eq([:once, :first])
+      expect(one_arg_def.description).to eq('')
+      expect(one_arg_def.params).to eq(['The first argument'])
+      expect(one_arg_def.condition_block).to be_nil
+      expect(one_arg_def.action_block).to be_a_kind_of(Proc)
+
+      expect(two_args_def.name).to eq(:two_args)
+      expect(two_args_def.aliases).to eq([])
+      expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE')
+      expect(two_args_def.params).to eq(['The first argument', 'The second argument'])
+      expect(two_args_def.condition_block).to be_nil
+      expect(two_args_def.action_block).to be_a_kind_of(Proc)
+
+      expect(cc_def.name).to eq(:cc)
+      expect(cc_def.aliases).to eq([])
+      expect(cc_def.description).to eq('')
+      expect(cc_def.params).to eq([])
+      expect(cc_def.condition_block).to be_nil
+      expect(cc_def.action_block).to be_nil
+
+      expect(cond_action_def.name).to eq(:cond_action)
+      expect(cond_action_def.aliases).to eq([])
+      expect(cond_action_def.description).to eq('')
+      expect(cond_action_def.params).to eq([])
+      expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
+      expect(cond_action_def.action_block).to be_a_kind_of(Proc)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e4954c4af8dcb9cbb427c6ee3aa2dccb6fe16c6
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb
@@ -0,0 +1,215 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Extractor do
+  let(:definitions) do
+    Class.new do
+      include Gitlab::SlashCommands::Dsl
+
+      command(:reopen, :open) { }
+      command(:assign) { }
+      command(:labels) { }
+      command(:power) { }
+    end.command_definitions
+  end
+
+  let(:extractor) { described_class.new(definitions) }
+
+  shared_examples 'command with no argument' do
+    it 'extracts command' do
+      msg, commands = extractor.extract_commands(original_msg)
+
+      expect(commands).to eq [['reopen']]
+      expect(msg).to eq final_msg
+    end
+  end
+
+  shared_examples 'command with a single argument' do
+    it 'extracts command' do
+      msg, commands = extractor.extract_commands(original_msg)
+
+      expect(commands).to eq [['assign', '@joe']]
+      expect(msg).to eq final_msg
+    end
+  end
+
+  shared_examples 'command with multiple arguments' do
+    it 'extracts command' do
+      msg, commands = extractor.extract_commands(original_msg)
+
+      expect(commands).to eq [['labels', '~foo ~"bar baz" label']]
+      expect(msg).to eq final_msg
+    end
+  end
+
+  describe '#extract_commands' do
+    describe 'command with no argument' do
+      context 'at the start of content' do
+        it_behaves_like 'command with no argument' do
+          let(:original_msg) { "/reopen\nworld" }
+          let(:final_msg) { "world" }
+        end
+      end
+
+      context 'in the middle of content' do
+        it_behaves_like 'command with no argument' do
+          let(:original_msg) { "hello\n/reopen\nworld" }
+          let(:final_msg) { "hello\nworld" }
+        end
+      end
+
+      context 'in the middle of a line' do
+        it 'does not extract command' do
+          msg = "hello\nworld /reopen"
+          msg, commands = extractor.extract_commands(msg)
+
+          expect(commands).to be_empty
+          expect(msg).to eq "hello\nworld /reopen"
+        end
+      end
+
+      context 'at the end of content' do
+        it_behaves_like 'command with no argument' do
+          let(:original_msg) { "hello\n/reopen" }
+          let(:final_msg) { "hello" }
+        end
+      end
+    end
+
+    describe 'command with a single argument' do
+      context 'at the start of content' do
+        it_behaves_like 'command with a single argument' do
+          let(:original_msg) { "/assign @joe\nworld" }
+          let(:final_msg) { "world" }
+        end
+      end
+
+      context 'in the middle of content' do
+        it_behaves_like 'command with a single argument' do
+          let(:original_msg) { "hello\n/assign @joe\nworld" }
+          let(:final_msg) { "hello\nworld" }
+        end
+      end
+
+      context 'in the middle of a line' do
+        it 'does not extract command' do
+          msg = "hello\nworld /assign @joe"
+          msg, commands = extractor.extract_commands(msg)
+
+          expect(commands).to be_empty
+          expect(msg).to eq "hello\nworld /assign @joe"
+        end
+      end
+
+      context 'at the end of content' do
+        it_behaves_like 'command with a single argument' do
+          let(:original_msg) { "hello\n/assign @joe" }
+          let(:final_msg) { "hello" }
+        end
+      end
+
+      context 'when argument is not separated with a space' do
+        it 'does not extract command' do
+          msg = "hello\n/assign@joe\nworld"
+          msg, commands = extractor.extract_commands(msg)
+
+          expect(commands).to be_empty
+          expect(msg).to eq "hello\n/assign@joe\nworld"
+        end
+      end
+    end
+
+    describe 'command with multiple arguments' do
+      context 'at the start of content' do
+        it_behaves_like 'command with multiple arguments' do
+          let(:original_msg) { %(/labels ~foo ~"bar baz" label\nworld) }
+          let(:final_msg) { "world" }
+        end
+      end
+
+      context 'in the middle of content' do
+        it_behaves_like 'command with multiple arguments' do
+          let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label\nworld) }
+          let(:final_msg) { "hello\nworld" }
+        end
+      end
+
+      context 'in the middle of a line' do
+        it 'does not extract command' do
+          msg = %(hello\nworld /labels ~foo ~"bar baz" label)
+          msg, commands = extractor.extract_commands(msg)
+
+          expect(commands).to be_empty
+          expect(msg).to eq %(hello\nworld /labels ~foo ~"bar baz" label)
+        end
+      end
+
+      context 'at the end of content' do
+        it_behaves_like 'command with multiple arguments' do
+          let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label) }
+          let(:final_msg) { "hello" }
+        end
+      end
+
+      context 'when argument is not separated with a space' do
+        it 'does not extract command' do
+          msg = %(hello\n/labels~foo ~"bar baz" label\nworld)
+          msg, commands = extractor.extract_commands(msg)
+
+          expect(commands).to be_empty
+          expect(msg).to eq %(hello\n/labels~foo ~"bar baz" label\nworld)
+        end
+      end
+    end
+
+    it 'extracts command with multiple arguments and various prefixes' do
+      msg = %(hello\n/power @user.name %9.10 ~"bar baz.2"\nworld)
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']]
+      expect(msg).to eq "hello\nworld"
+    end
+
+    it 'extracts multiple commands' do
+      msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2" label'], ['reopen']]
+      expect(msg).to eq "hello\nworld"
+    end
+
+    it 'does not alter original content if no command is found' do
+      msg = 'Fixes #123'
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to be_empty
+      expect(msg).to eq 'Fixes #123'
+    end
+
+    it 'does not extract commands inside a blockcode' do
+      msg = "Hello\r\n```\r\nThis is some text\r\n/close\r\n/assign @user\r\n```\r\n\r\nWorld"
+      expected = msg.delete("\r")
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to be_empty
+      expect(msg).to eq expected
+    end
+
+    it 'does not extract commands inside a blockquote' do
+      msg = "Hello\r\n>>>\r\nThis is some text\r\n/close\r\n/assign @user\r\n>>>\r\n\r\nWorld"
+      expected = msg.delete("\r")
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to be_empty
+      expect(msg).to eq expected
+    end
+
+    it 'does not extract commands inside a HTML tag' do
+      msg = "Hello\r\n<div>\r\nThis is some text\r\n/close\r\n/assign @user\r\n</div>\r\n\r\nWorld"
+      expected = msg.delete("\r")
+      msg, commands = extractor.extract_commands(msg)
+
+      expect(commands).to be_empty
+      expect(msg).to eq expected
+    end
+  end
+end
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index e86b9ef6a63497de1e6b31b5825a2b3bc188a762..b661a894c0c85a861c80d5d70f91f418303fc5b1 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -5,12 +5,6 @@ describe Gitlab::SnippetSearchResults do
 
   let(:results) { described_class.new(Snippet.all, 'foo') }
 
-  describe '#total_count' do
-    it 'returns the total amount of search hits' do
-      expect(results.total_count).to eq(2)
-    end
-  end
-
   describe '#snippet_titles_count' do
     it 'returns the amount of matched snippet titles' do
       expect(results.snippet_titles_count).to eq(1)
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index f770857e958817206b6687da1a5f1cd5ef66c73f..d2d334e64131d41034b0eec7daebf65e77f3804b 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do
   let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
 
   before do
-    project.team.add_user(user, Gitlab::Access::MASTER)
+    project.add_user(user, Gitlab::Access::MASTER)
     project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
     project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
     project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do
 
     context 'when repo is bare or empty' do
       let(:empty_project) { create(:empty_project) }
-      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+      before { empty_project.add_user(user, Gitlab::Access::MASTER) }
 
       it "returns empty array" do
         templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do
     context "when repo is empty" do
       let(:empty_project) { create(:empty_project) }
 
-      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+      before { empty_project.add_user(user, Gitlab::Access::MASTER) }
 
       it "raises file not found" do
         issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index bb0f68043fa0ef7ae0f5b8c6b4c419dc87b6684c..ddf68c4cf78bfc0ab8d16b706fac05e8f0af25b6 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do
   let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
 
   before do
-    project.team.add_user(user, Gitlab::Access::MASTER)
+    project.add_user(user, Gitlab::Access::MASTER)
     project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
     project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
     project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do
 
     context 'when repo is bare or empty' do
       let(:empty_project) { create(:empty_project) }
-      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+      before { empty_project.add_user(user, Gitlab::Access::MASTER) }
 
       it "returns empty array" do
         templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do
     context "when repo is empty" do
       let(:empty_project) { create(:empty_project) }
 
-      before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+      before { empty_project.add_user(user, Gitlab::Access::MASTER) }
 
       it "raises file not found" do
         issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d5d87310874c0d145ae09a3e81762d39c3658526
--- /dev/null
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -0,0 +1,35 @@
+describe Gitlab::Utils, lib: true do
+  def to_boolean(value)
+    described_class.to_boolean(value)
+  end
+
+  describe '.to_boolean' do
+    it 'accepts booleans' do
+      expect(to_boolean(true)).to be(true)
+      expect(to_boolean(false)).to be(false)
+    end
+
+    it 'converts a valid string to a boolean' do
+      expect(to_boolean(true)).to be(true)
+      expect(to_boolean('true')).to be(true)
+      expect(to_boolean('YeS')).to be(true)
+      expect(to_boolean('t')).to be(true)
+      expect(to_boolean('1')).to be(true)
+      expect(to_boolean('ON')).to be(true)
+
+      expect(to_boolean('FaLse')).to be(false)
+      expect(to_boolean('F')).to be(false)
+      expect(to_boolean('NO')).to be(false)
+      expect(to_boolean('n')).to be(false)
+      expect(to_boolean('0')).to be(false)
+      expect(to_boolean('oFF')).to be(false)
+    end
+
+    it 'converts an invalid string to nil' do
+      expect(to_boolean('fals')).to be_nil
+      expect(to_boolean('yeah')).to be_nil
+      expect(to_boolean('')).to be_nil
+      expect(to_boolean(nil)).to be_nil
+    end
+  end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index c5c1402e8fcfc5d3d02aadb5859e93d395d1e401..b5b685da904ffc787c10676a02c4f25f05a97a51 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,18 +1,141 @@
 require 'spec_helper'
 
 describe Gitlab::Workhorse, lib: true do
-  let(:project) { create(:project) }
-  let(:subject) { Gitlab::Workhorse }
+  let(:project)    { create(:project) }
+  let(:repository) { project.repository }
 
-  describe "#send_git_archive" do
+  def decode_workhorse_header(array)
+    key, value = array
+    command, encoded_params = value.split(":")
+    params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
+
+    [key, command, params]
+  end
+
+  describe ".send_git_archive" do
     context "when the repository doesn't have an archive file path" do
       before do
         allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
       end
 
       it "raises an error" do
-        expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+        expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+      end
+    end
+  end
+
+  describe '.send_git_patch' do
+    let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+    subject { described_class.send_git_patch(repository, diff_refs) }
+
+    it 'sets the header correctly' do
+      key, command, params = decode_workhorse_header(subject)
+
+      expect(key).to eq("Gitlab-Workhorse-Send-Data")
+      expect(command).to eq("git-format-patch")
+      expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+    end
+  end
+
+  describe '.send_git_diff' do
+    let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+    subject { described_class.send_git_patch(repository, diff_refs) }
+
+    it 'sets the header correctly' do
+      key, command, params = decode_workhorse_header(subject)
+
+      expect(key).to eq("Gitlab-Workhorse-Send-Data")
+      expect(command).to eq("git-format-patch")
+      expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+    end
+  end
+
+  describe ".secret" do
+    subject { described_class.secret }
+
+    before do
+      described_class.instance_variable_set(:@secret, nil)
+      described_class.write_secret
+    end
+
+    it 'returns 32 bytes' do
+      expect(subject).to be_a(String)
+      expect(subject.length).to eq(32)
+      expect(subject.encoding).to eq(Encoding::ASCII_8BIT)
+    end
+
+    it 'accepts a trailing newline' do
+      open(described_class.secret_path, 'a') { |f| f.write "\n" }
+      expect(subject.length).to eq(32)
+    end
+
+    it 'raises an exception if the secret file cannot be read' do
+      File.delete(described_class.secret_path)
+      expect { subject }.to raise_exception(Errno::ENOENT)
+    end
+
+    it 'raises an exception if the secret file contains the wrong number of bytes' do
+      File.truncate(described_class.secret_path, 0)
+      expect { subject }.to raise_exception(RuntimeError)
+    end
+  end
+
+  describe ".write_secret" do
+    let(:secret_path) { described_class.secret_path }
+    before do
+      begin
+        File.delete(secret_path)
+      rescue Errno::ENOENT
       end
+
+      described_class.write_secret
+    end
+
+    it 'uses mode 0600' do
+      expect(File.stat(secret_path).mode & 0777).to eq(0600)
+    end
+
+    it 'writes base64 data' do
+      bytes = Base64.strict_decode64(File.read(secret_path))
+      expect(bytes).not_to be_empty
+    end
+  end
+
+  describe '#verify_api_request!' do
+    let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER }
+    let(:payload) { { 'iss' => 'gitlab-workhorse' } }
+
+    it 'accepts a correct header' do
+      headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+      expect { call_verify(headers) }.not_to raise_error
+    end
+
+    it 'raises an error when the header is not set' do
+      expect { call_verify({}) }.to raise_jwt_error
+    end
+
+    it 'raises an error when the header is not signed' do
+      headers = { header_key => JWT.encode(payload, nil, 'none') }
+      expect { call_verify(headers) }.to raise_jwt_error
+    end
+
+    it 'raises an error when the header is signed with the wrong key' do
+      headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') }
+      expect { call_verify(headers) }.to raise_jwt_error
+    end
+
+    it 'raises an error when the issuer is incorrect' do
+      payload['iss'] = 'somebody else'
+      headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+      expect { call_verify(headers) }.to raise_jwt_error
+    end
+
+    def raise_jwt_error
+      raise_error(JWT::DecodeError)
+    end
+
+    def call_verify(headers)
+      described_class.verify_api_request!(headers)
     end
   end
 end
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
index 0df89938e971df1e198d645ae15dd1e72e04a86d..d968096783c4a08c0c1a81991b80e712af0e5cf1 100644
--- a/spec/mailers/emails/builds_spec.rb
+++ b/spec/mailers/emails/builds_spec.rb
@@ -1,6 +1,5 @@
 require 'spec_helper'
 require 'email_spec'
-require 'mailers/shared/notify'
 
 describe Notify do
   include EmailSpec::Matchers
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e22858d1d8fbbdcb8c229e3e0dc786aaf73856f5
--- /dev/null
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require 'email_spec'
+
+describe Notify, "merge request notifications" do
+  include EmailSpec::Matchers
+
+  describe "#resolved_all_discussions_email" do
+    let(:user) { create(:user) }
+    let(:merge_request) { create(:merge_request) }
+    let(:current_user) { create(:user) }
+
+    subject { Notify.resolved_all_discussions_email(user.id, merge_request.id, current_user.id) }
+
+    it "includes the name of the resolver" do
+      expect(subject).to have_body_text current_user.name
+    end
+  end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 781472d0c000841a465d4819ce0c15bb07b63bda..14bc062ef125eaa7c1c3f5c43be5e02f98f19fff 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,6 +1,5 @@
 require 'spec_helper'
 require 'email_spec'
-require 'mailers/shared/notify'
 
 describe Notify do
   include EmailSpec::Matchers
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index fa241867858744f5eb48e18745bad22059e4230e..f5f3f58613d6d9718b9807aab5d428af40f2341f 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1,6 +1,5 @@
 require 'spec_helper'
 require 'email_spec'
-require 'mailers/shared/notify'
 
 describe Notify do
   include EmailSpec::Helpers
@@ -402,7 +401,7 @@ describe Notify do
 
     describe 'project access requested' do
       context 'for a project in a user namespace' do
-        let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+        let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
         let(:user) { create(:user) }
         let(:project_member) do
           project.request_access(user)
@@ -429,7 +428,7 @@ describe Notify do
       context 'for a project in a group' do
         let(:group_owner) { create(:user) }
         let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
-        let(:project) { create(:project, namespace: group) }
+        let(:project) { create(:project, :public, namespace: group) }
         let(:user) { create(:user) }
         let(:project_member) do
           project.request_access(user)
@@ -492,16 +491,22 @@ describe Notify do
       end
     end
 
-    def invite_to_project(project:, email:, inviter:)
-      ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
-
-      project.project_members.invite.last
+    def invite_to_project(project, inviter:)
+      create(
+        :project_member,
+        :developer,
+        project: project,
+        invite_token: '1234',
+        invite_email: 'toto@example.com',
+        user: nil,
+        created_by: inviter
+      )
     end
 
     describe 'project invitation' do
       let(:project) { create(:project) }
       let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
-      let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+      let(:project_member) { invite_to_project(project, inviter: master) }
 
       subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
 
@@ -520,10 +525,10 @@ describe Notify do
 
     describe 'project invitation accepted' do
       let(:project) { create(:project) }
-      let(:invited_user) { create(:user) }
+      let(:invited_user) { create(:user, name: 'invited user') }
       let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
       let(:project_member) do
-        invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+        invitee = invite_to_project(project, inviter: master)
         invitee.accept_invite!(invited_user)
         invitee
       end
@@ -547,7 +552,7 @@ describe Notify do
       let(:project) { create(:project) }
       let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
       let(:project_member) do
-        invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+        invitee = invite_to_project(project, inviter: master)
         invitee.decline_invite!
         invitee
       end
@@ -622,7 +627,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 /#{commit.title} \(#{commit.short_id}\)/
+          is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/
         end
 
         it 'contains a link to the commit' do
@@ -739,16 +744,22 @@ describe Notify do
       end
     end
 
-    def invite_to_group(group:, email:, inviter:)
-      GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
-
-      group.group_members.invite.last
+    def invite_to_group(group, inviter:)
+      create(
+        :group_member,
+        :developer,
+        group: group,
+        invite_token: '1234',
+        invite_email: 'toto@example.com',
+        user: nil,
+        created_by: inviter
+      )
     end
 
     describe 'group invitation' do
       let(:group) { create(:group) }
       let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
-      let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+      let(:group_member) { invite_to_group(group, inviter: owner) }
 
       subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
 
@@ -767,10 +778,10 @@ describe Notify do
 
     describe 'group invitation accepted' do
       let(:group) { create(:group) }
-      let(:invited_user) { create(:user) }
+      let(:invited_user) { create(:user, name: 'invited user') }
       let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
       let(:group_member) do
-        invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+        invitee = invite_to_group(group, inviter: owner)
         invitee.accept_invite!(invited_user)
         invitee
       end
@@ -794,7 +805,7 @@ describe Notify do
       let(:group) { create(:group) }
       let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
       let(:group_member) do
-        invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+        invitee = invite_to_group(group, inviter: owner)
         invitee.decline_invite!
         invitee
       end
@@ -819,6 +830,7 @@ describe Notify do
     let(:user) { create(:user, email: 'old-email@mail.com') }
 
     before do
+      stub_config_setting(email_subject_suffix: 'A Nice Suffix')
       perform_enqueued_jobs do
         user.email = "new-email@mail.com"
         user.save
@@ -835,7 +847,7 @@ describe Notify do
     end
 
     it 'has the correct subject' do
-      is_expected.to have_subject "Confirmation instructions"
+      is_expected.to have_subject /^Confirmation instructions/
     end
 
     it 'includes a link to the site' do
@@ -851,7 +863,7 @@ describe Notify do
     subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) }
 
     it_behaves_like 'it should not have Gmail Actions links'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+    it_behaves_like 'a user cannot unsubscribe through footer link'
     it_behaves_like 'an email with X-GitLab headers containing project details'
     it_behaves_like 'an email that contains a header with author username'
 
@@ -904,7 +916,7 @@ describe Notify do
     subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
 
     it_behaves_like 'it should not have Gmail Actions links'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+    it_behaves_like 'a user cannot unsubscribe through footer link'
     it_behaves_like 'an email with X-GitLab headers containing project details'
     it_behaves_like 'an email that contains a header with author username'
 
@@ -926,7 +938,7 @@ describe Notify do
     subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
 
     it_behaves_like 'it should not have Gmail Actions links'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+    it_behaves_like 'a user cannot unsubscribe through footer link'
     it_behaves_like 'an email with X-GitLab headers containing project details'
     it_behaves_like 'an email that contains a header with author username'
 
@@ -954,7 +966,7 @@ describe Notify do
     subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
 
     it_behaves_like 'it should not have Gmail Actions links'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+    it_behaves_like 'a user cannot unsubscribe through footer link'
     it_behaves_like 'an email with X-GitLab headers containing project details'
     it_behaves_like 'an email that contains a header with author username'
 
@@ -1056,7 +1068,7 @@ describe Notify do
     subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
 
     it_behaves_like 'it should show Gmail Actions View Commit link'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+    it_behaves_like 'a user cannot unsubscribe through footer link'
     it_behaves_like 'an email with X-GitLab headers containing project details'
     it_behaves_like 'an email that contains a header with author username'
 
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 853f6943cef87546ed3c7099b83501b49b8774a8..1bdf005c8237dbaf87f9f8167bb2667dcccc4e5c 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -218,4 +218,17 @@ describe Ability, lib: true do
       end
     end
   end
+
+  describe '.project_disabled_features_rules' do
+    let(:project) { create(:project,  wiki_access_level: ProjectFeature::DISABLED) }
+
+    subject { described_class.allowed(project.owner, project) }
+
+    context 'wiki named abilities' do
+      it 'disables wiki abilities if the project has no wiki' do
+        expect(project).to receive(:has_external_wiki?).and_return(false)
+        expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
+      end
+    end
+  end
 end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 305f8bc88cc5e64d85a8ec94ba038c02a24c7957..c4486a3208266126637380e4bc1dbd930c32033b 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do
   describe 'associations' do
     it { is_expected.to belong_to(:reporter).class_name('User') }
     it { is_expected.to belong_to(:user) }
+
+    it "aliases reporter to author" do
+      expect(subject.author).to be(subject.reporter)
+    end
   end
 
   describe 'validations' do
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index c5658bd26e1e98be958fcef657ed9e4f9f736246..0b72a2f979b24d442bd44c83b2999313bb29d45f 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe Appearance, type: :model do
-  subject { create(:appearance) }
+  subject { build(:appearance) }
 
   it { is_expected.to be_valid }
 
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index cc215d252f9cec3e3118a32eb6a7b4e58c1108dd..b950fcdd81aae02426e796e1ac8193872ad84afa 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -41,14 +41,80 @@ describe ApplicationSetting, models: true do
       subject { setting }
     end
 
-    context 'repository storages inclussion' do
+    # Upgraded databases will have this sort of content
+    context 'repository_storages is a String, not an Array' do
+      before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') }
+
+      it { expect(setting.repository_storages_before_type_cast).to eq('default') }
+      it { expect(setting.repository_storages).to eq(['default']) }
+    end
+
+    context 'repository storages' do
       before do
-        storages = { 'custom' => 'tmp/tests/custom_repositories' }
+        storages = {
+          'custom1' => 'tmp/tests/custom_repositories_1',
+          'custom2' => 'tmp/tests/custom_repositories_2',
+          'custom3' => 'tmp/tests/custom_repositories_3',
+
+        }
         allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
       end
 
-      it { is_expected.to allow_value('custom').for(:repository_storage) }
-      it { is_expected.not_to allow_value('alternative').for(:repository_storage) }
+      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.not_to allow_value('alternative').for(:repository_storages) }
+        it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) }
+      end
+
+      describe 'presence' do
+        it { is_expected.not_to allow_value([]).for(:repository_storages) }
+        it { is_expected.not_to allow_value("").for(:repository_storages) }
+        it { is_expected.not_to allow_value(nil).for(:repository_storages) }
+      end
+
+      describe '.pick_repository_storage' do
+        it 'uses Array#sample to pick a random storage' do
+          array = double('array', sample: 'random')
+          expect(setting).to receive(:repository_storages).and_return(array)
+
+          expect(setting.pick_repository_storage).to eq('random')
+        end
+
+        describe '#repository_storage' do
+          it 'returns the first storage' do
+            setting.repository_storages = ['good', 'bad']
+
+            expect(setting.repository_storage).to eq('good')
+          end
+        end
+
+        describe '#repository_storage=' do
+          it 'overwrites repository_storages' do
+            setting.repository_storage = 'overwritten'
+
+            expect(setting.repository_storages).to eq(['overwritten'])
+          end
+        end
+      end
+    end
+
+    context 'housekeeping settings' do
+      it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) }
+
+      it 'wants the full repack period to be longer than the incremental repack period' do
+        subject.housekeeping_incremental_repack_period = 2
+        subject.housekeeping_full_repack_period = 1
+
+        expect(subject).not_to be_valid
+      end
+
+      it 'wants the gc period to be longer than the full repack period' do
+        subject.housekeeping_full_repack_period = 2
+        subject.housekeeping_gc_period = 1
+
+        expect(subject).not_to be_valid
+      end
     end
   end
 
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index cee20234e1f8fae94929c46f503880627921d638..03d02b4d382bc25ca85cc41ff39dafe77f8f3a98 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
 require 'rails_helper'
 
 describe Blob do
@@ -7,6 +8,25 @@ describe Blob do
     end
   end
 
+  describe '#data' do
+    context 'using a binary blob' do
+      it 'returns the data as-is' do
+        data = "\n\xFF\xB9\xC3"
+        blob = described_class.new(double(binary?: true, data: data))
+
+        expect(blob.data).to eq(data)
+      end
+    end
+
+    context 'using a text blob' do
+      it 'converts the data to UTF-8' do
+        blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+
+        expect(blob.data).to eq("\n���")
+      end
+    end
+  end
+
   describe '#svg?' do
     it 'is falsey when not text' do
       git_blob = double(text?: false)
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..12d29540137c41dbf643f2a3b24562a61c0afce9
--- /dev/null
+++ b/spec/models/board_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe Board do
+  describe 'relationships' do
+    it { is_expected.to belong_to(:project) }
+    it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:project) }
+  end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 72688137f08f531857a9e6c16cd54683170fec9a..02d6263094aa0d784e3268deef207cd1ffd88880 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
 require 'spec_helper'
 
 describe BroadcastMessage, models: true do
-  include ActiveSupport::Testing::TimeHelpers
-
   subject { create(:broadcast_message) }
 
   it { is_expected.to be_valid }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ee2c3d049843bc5d1f17eee86b6c3ebe4a3cdc73..ae185de9ca379311cdcbc0d931e84c4f4c3524d1 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -39,8 +39,8 @@ describe Ci::Build, models: true do
     end
   end
 
-  describe '#ignored?' do
-    subject { build.ignored? }
+  describe '#failed_but_allowed?' do
+    subject { build.failed_but_allowed? }
 
     context 'when build is not allowed to fail' do
       before do
@@ -88,9 +88,7 @@ describe Ci::Build, models: true do
   end
 
   describe '#trace' do
-    subject { build.trace_html }
-
-    it { is_expected.to be_empty }
+    it { expect(build.trace).to be_nil }
 
     context 'when build.trace contains text' do
       let(:text) { 'example output' }
@@ -98,16 +96,80 @@ describe Ci::Build, models: true do
         build.trace = text
       end
 
-      it { is_expected.to include(text) }
-      it { expect(subject.length).to be >= text.length }
+      it { expect(build.trace).to eq(text) }
+    end
+
+    context 'when build.trace hides runners token' do
+      let(:token) { 'my_secret_token' }
+
+      before do
+        build.update(trace: token)
+        build.project.update(runners_token: token)
+      end
+
+      it { expect(build.trace).not_to include(token) }
+      it { expect(build.raw_trace).to include(token) }
+    end
+
+    context 'when build.trace hides build token' do
+      let(:token) { 'my_secret_token' }
+
+      before do
+        build.update(trace: token)
+        build.update(token: token)
+      end
+
+      it { expect(build.trace).not_to include(token) }
+      it { expect(build.raw_trace).to include(token) }
+    end
+  end
+
+  describe '#raw_trace' do
+    subject { build.raw_trace }
+
+    context 'when build.trace hides runners token' do
+      let(:token) { 'my_secret_token' }
+
+      before do
+        build.project.update(runners_token: token)
+        build.update(trace: token)
+      end
+
+      it { is_expected.not_to include(token) }
     end
 
-    context 'when build.trace hides token' do
+    context 'when build.trace hides build token' do
       let(:token) { 'my_secret_token' }
 
       before do
-        build.project.update_attributes(runners_token: token)
-        build.update_attributes(trace: token)
+        build.update(token: token)
+        build.update(trace: token)
+      end
+
+      it { is_expected.not_to include(token) }
+    end
+  end
+
+  context '#append_trace' do
+    subject { build.trace_html }
+
+    context 'when build.trace hides runners token' do
+      let(:token) { 'my_secret_token' }
+
+      before do
+        build.project.update(runners_token: token)
+        build.append_trace(token, 0)
+      end
+
+      it { is_expected.not_to include(token) }
+    end
+
+    context 'when build.trace hides build token' do
+      let(:token) { 'my_secret_token' }
+
+      before do
+        build.update(token: token)
+        build.append_trace(token, 0)
       end
 
       it { is_expected.not_to include(token) }
@@ -231,6 +293,34 @@ describe Ci::Build, models: true do
       it { is_expected.to eq(predefined_variables) }
     end
 
+    context 'when build has user' do
+      let(:user) { create(:user, username: 'starter') }
+      let(:user_variables) do
+        [
+          { key: 'GITLAB_USER_ID',    value: user.id.to_s, public: true },
+          { key: 'GITLAB_USER_EMAIL', value: user.email,   public: true }
+        ]
+      end
+
+      before do
+        build.update_attributes(user: user)
+      end
+
+      it { user_variables.each { |v| is_expected.to include(v) } }
+    end
+
+    context 'when build started manually' do
+      before do
+        build.update_attributes(when: :manual)
+      end
+
+      let(:manual_variable) do
+        { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+      end
+
+      it { is_expected.to include(manual_variable) }
+    end
+
     context 'when build is for tag' do
       let(:tag_variable) do
         { key: 'CI_BUILD_TAG', value: 'master', public: true }
@@ -948,15 +1038,17 @@ describe Ci::Build, models: true do
       before { build.run! }
 
       it 'returns false' do
-        expect(build.retryable?).to be false
+        expect(build).not_to be_retryable
       end
     end
 
     context 'when build is finished' do
-      before { build.success! }
+      before do
+        build.success!
+      end
 
       it 'returns true' do
-        expect(build.retryable?).to be true
+        expect(build).to be_retryable
       end
     end
   end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 36d10636ae9071ebddb2883636d20bb91278c78a..a37a00f461a888f7b3b61390b4874681b93a8e2f 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -8,7 +8,7 @@ describe Ci::Build, models: true do
     it 'obfuscates project runners token' do
       allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
 
-      expect(build.trace).to eq("Test: xxxxxx")
+      expect(build.trace).to eq("Test: xxxxxxxxxxxxxxxxxxxx")
     end
 
     it 'empty project runners token' do
@@ -19,4 +19,64 @@ describe Ci::Build, models: true do
       expect(build.trace).to eq(test_trace)
     end
   end
+
+  describe '#has_trace_file?' do
+    context 'when there is no trace' do
+      it { expect(build.has_trace_file?).to be_falsey }
+      it { expect(build.trace).to be_nil }
+    end
+
+    context 'when there is a trace' do
+      context 'when trace is stored in file' do
+        let(:build_with_trace) { create(:ci_build, :trace) }
+
+        it { expect(build_with_trace.has_trace_file?).to be_truthy }
+        it { expect(build_with_trace.trace).to eq('BUILD TRACE') }
+      end
+
+      context 'when trace is stored in old file' do
+        before do
+          allow(build.project).to receive(:ci_id).and_return(999)
+          allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+          allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true)
+          allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace)
+        end
+
+        it { expect(build.has_trace_file?).to be_truthy }
+        it { expect(build.trace).to eq(test_trace) }
+      end
+
+      context 'when trace is stored in DB' do
+        before do
+          allow(build.project).to receive(:ci_id).and_return(nil)
+          allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace)
+          allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+          allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false)
+        end
+
+        it { expect(build.has_trace_file?).to be_falsey }
+        it { expect(build.trace).to eq(test_trace) }
+      end
+    end
+  end
+
+  describe '#trace_file_path' do
+    context 'when trace is stored in file' do
+      before do
+        allow(build).to receive(:has_trace_file?).and_return(true)
+        allow(build).to receive(:has_old_trace_file?).and_return(false)
+      end
+
+      it { expect(build.trace_file_path).to eq(build.path_to_trace) }
+    end
+
+    context 'when trace is stored in old file' do
+      before do
+        allow(build).to receive(:has_trace_file?).and_return(true)
+        allow(build).to receive(:has_old_trace_file?).and_return(true)
+      end
+
+      it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
+    end
+  end
 end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8137e9f8f71abddc964c9da950eb975140ef3f31..71b7628ef10269fd95acc1e21b9c9c95de1bb91b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -88,24 +88,38 @@ describe Ci::Pipeline, models: true do
 
     context 'no failed builds' do
       before do
-        FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+        create_build('rspec', 'success')
       end
 
-      it 'be not retryable' do
+      it 'is not retryable' do
         is_expected.to be_falsey
       end
+
+      context 'one canceled job' do
+        before do
+          create_build('rubocop', 'canceled')
+        end
+
+        it 'is retryable' do
+          is_expected.to be_truthy
+        end
+      end
     end
 
     context 'with failed builds' do
       before do
-        FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
-        FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+        create_build('rspec', 'running')
+        create_build('rubocop', 'failed')
       end
 
-      it 'be retryable' do
+      it 'is retryable' do
         is_expected.to be_truthy
       end
     end
+
+    def create_build(name, status)
+      create(:ci_build, name: name, status: status, pipeline: pipeline)
+    end
   end
 
   describe '#stages' do
@@ -124,17 +138,32 @@ describe Ci::Pipeline, models: true do
 
   describe 'state machine' do
     let(:current) { Time.now.change(usec: 0) }
-    let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
-    let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
+    let(:build) { create_build('build1', 0) }
+    let(:build_b) { create_build('build2', 0) }
+    let(:build_c) { create_build('build3', 0) }
 
     describe '#duration' do
       before do
-        build.skip
-        build2.skip
+        travel_to(current + 30) do
+          build.run!
+          build.success!
+          build_b.run!
+          build_c.run!
+        end
+
+        travel_to(current + 40) do
+          build_b.drop!
+        end
+
+        travel_to(current + 70) do
+          build_c.success!
+        end
       end
 
       it 'matches sum of builds duration' do
-        expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+        pipeline.reload
+
+        expect(pipeline.duration).to eq(40)
       end
     end
 
@@ -165,6 +194,36 @@ describe Ci::Pipeline, models: true do
         expect(pipeline.reload.finished_at).to be_nil
       end
     end
+
+    describe 'merge request metrics' do
+      let(:project) { FactoryGirl.create :project }
+      let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+      let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+      before do
+        expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
+      end
+
+      context 'when transitioning to running' do
+        it 'schedules metrics workers' do
+          pipeline.run
+        end
+      end
+
+      context 'when transitioning to success' do
+        it 'schedules metrics workers' do
+          pipeline.succeed
+        end
+      end
+    end
+
+    def create_build(name, queued_at = current, started_from = 0)
+      create(:ci_build,
+             name: name,
+             pipeline: pipeline,
+             queued_at: queued_at,
+             started_at: queued_at + started_from)
+    end
   end
 
   describe '#branch?' do
@@ -191,6 +250,36 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  context 'with non-empty project' do
+    let(:project) { create(:project) }
+
+    let(:pipeline) do
+      create(:ci_pipeline,
+             project: project,
+             ref: project.default_branch,
+             sha: project.commit.sha)
+    end
+
+    describe '#latest?' do
+      context 'with latest sha' do
+        it 'returns true' do
+          expect(pipeline).to be_latest
+        end
+      end
+
+      context 'with not latest sha' do
+        before do
+          pipeline.update(
+            sha: project.commit("#{project.default_branch}~1").sha)
+        end
+
+        it 'returns false' do
+          expect(pipeline).not_to be_latest
+        end
+      end
+    end
+  end
+
   describe '#manual_actions' do
     subject { pipeline.manual_actions }
 
@@ -314,8 +403,8 @@ describe Ci::Pipeline, models: true do
   end
 
   describe '#execute_hooks' do
-    let!(:build_a) { create_build('a') }
-    let!(:build_b) { create_build('b') }
+    let!(:build_a) { create_build('a', 0) }
+    let!(:build_b) { create_build('b', 1) }
 
     let!(:hook) do
       create(:project_hook, project: project, pipeline_events: enabled)
@@ -339,7 +428,7 @@ describe Ci::Pipeline, models: true do
             build_b.enqueue
           end
 
-          it 'receive a pending event once' do
+          it 'receives a pending event once' do
             expect(WebMock).to have_requested_pipeline_hook('pending').once
           end
         end
@@ -352,7 +441,7 @@ describe Ci::Pipeline, models: true do
             build_b.run
           end
 
-          it 'receive a running event once' do
+          it 'receives a running event once' do
             expect(WebMock).to have_requested_pipeline_hook('running').once
           end
         end
@@ -360,14 +449,26 @@ describe Ci::Pipeline, models: true do
         context 'when all builds succeed' do
           before do
             build_a.success
-            build_b.success
+
+            # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker
+            build_b.reload.success
           end
 
-          it 'receive a success event once' do
+          it 'receives a success event once' do
             expect(WebMock).to have_requested_pipeline_hook('success').once
           end
         end
 
+        context 'when stage one failed' do
+          before do
+            build_a.drop
+          end
+
+          it 'receives a failed event once' do
+            expect(WebMock).to have_requested_pipeline_hook('failed').once
+          end
+        end
+
         def have_requested_pipeline_hook(status)
           have_requested(:post, hook.url).with do |req|
             json_body = JSON.parse(req.body)
@@ -391,8 +492,110 @@ describe Ci::Pipeline, models: true do
       end
     end
 
-    def create_build(name)
-      create(:ci_build, :created, pipeline: pipeline, name: name)
+    def create_build(name, stage_idx)
+      create(:ci_build,
+             :created,
+             pipeline: pipeline,
+             name: name,
+             stage_idx: stage_idx)
+    end
+  end
+
+  describe "#merge_requests" do
+    let(:project) { FactoryGirl.create :project }
+    let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+
+    it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
+      merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+      expect(pipeline.merge_requests).to eq([merge_request])
+    end
+
+    it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
+      create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+      expect(pipeline.merge_requests).to be_empty
+    end
+
+    it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
+      create(:merge_request, source_project: project, source_branch: pipeline.ref)
+      allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
+
+      expect(pipeline.merge_requests).to be_empty
+    end
+  end
+
+  describe 'notifications when pipeline success or failed' do
+    let(:project) { create(:project) }
+
+    let(:pipeline) do
+      create(:ci_pipeline,
+             project: project,
+             sha: project.commit('master').sha,
+             user: create(:user))
+    end
+
+    before do
+      reset_delivered_emails!
+
+      project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+
+      perform_enqueued_jobs do
+        pipeline.enqueue
+        pipeline.run
+      end
+    end
+
+    shared_examples 'sending a notification' do
+      it 'sends an email' do
+        should_only_email(pipeline.user, kind: :bcc)
+      end
+    end
+
+    shared_examples 'not sending any notification' do
+      it 'does not send any email' do
+        should_not_email_anyone
+      end
+    end
+
+    context 'with success pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.succeed
+        end
+      end
+
+      it_behaves_like 'sending a notification'
+    end
+
+    context 'with failed pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.drop
+        end
+      end
+
+      it_behaves_like 'sending a notification'
+    end
+
+    context 'with skipped pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.skip
+        end
+      end
+
+      it_behaves_like 'not sending any notification'
+    end
+
+    context 'with cancelled pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.cancel
+        end
+      end
+
+      it_behaves_like 'not sending any notification'
     end
   end
 end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 384a38ebc6914187ff37cb12be36b73984b844bf..c41359b55a3b5224d3087afe2df2f510b00d6e1b 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -76,16 +76,6 @@ describe CommitRange, models: true do
     end
   end
 
-  describe '#reference_title' do
-    it 'returns the correct String for three-dot ranges' do
-      expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}"
-    end
-
-    it 'returns the correct String for two-dot ranges' do
-      expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}"
-    end
-  end
-
   describe '#to_param' do
     it 'includes the correct keys' do
       expect(range.to_param.keys).to eq %i(from to)
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index d3e6a6648cc647cc9c7aeac11e7edbbdcef6553b..e3bb3482d67b2f881a0bc310166344a0c895ab67 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -164,10 +164,10 @@ eos
     let(:data) { commit.hook_attrs(with_changed_files: true) }
 
     it { expect(data).to be_a(Hash) }
-    it { expect(data[:message]).to include('Add submodule from gitlab.com') }
-    it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
-    it { expect(data[:added]).to eq(["gitlab-grack"]) }
-    it { expect(data[:modified]).to eq([".gitmodules"]) }
+    it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
+    it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
+    it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
+    it { expect(data[:modified]).to eq([]) }
     it { expect(data[:removed]).to eq([]) }
   end
 
@@ -205,12 +205,53 @@ eos
     end
   end
 
-  describe '#ci_commits' do
-    # TODO: kamil
-  end
-
   describe '#status' do
-    # TODO: kamil
+    context 'without arguments for compound status' do
+      shared_examples 'giving the status from pipeline' do
+        it do
+          expect(commit.status).to eq(Ci::Pipeline.status)
+        end
+      end
+
+      context 'with pipelines' do
+        let!(:pipeline) do
+          create(:ci_empty_pipeline, project: project, sha: commit.sha)
+        end
+
+        it_behaves_like 'giving the status from pipeline'
+      end
+
+      context 'without pipelines' do
+        it_behaves_like 'giving the status from pipeline'
+      end
+    end
+
+    context 'when a particular ref is specified' do
+      let!(:pipeline_from_master) do
+        create(:ci_empty_pipeline,
+               project: project,
+               sha: commit.sha,
+               ref: 'master',
+               status: 'failed')
+      end
+
+      let!(:pipeline_from_fix) do
+        create(:ci_empty_pipeline,
+               project: project,
+               sha: commit.sha,
+               ref: 'fix',
+               status: 'success')
+      end
+
+      it 'gives pipelines from a particular branch' do
+        expect(commit.status('master')).to eq(pipeline_from_master.status)
+        expect(commit.status('fix')).to eq(pipeline_from_fix.status)
+      end
+
+      it 'gives compound status if ref is nil' do
+        expect(commit.status(nil)).to eq(commit.status)
+      end
+    end
   end
 
   describe '#participants' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index fcfa3138ce50b16cc2b0f5e7ae34acf15da63e5d..80c2a1bc7a987a32ca917c705dbf6724b6544010 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -7,7 +7,11 @@ describe CommitStatus, models: true do
     create(:ci_pipeline, project: project, sha: project.commit.id)
   end
 
-  let(:commit_status) { create(:commit_status, pipeline: pipeline) }
+  let(:commit_status) { create_status }
+
+  def create_status(args = {})
+    create(:commit_status, args.merge(pipeline: pipeline))
+  end
 
   it { is_expected.to belong_to(:pipeline) }
   it { is_expected.to belong_to(:user) }
@@ -40,7 +44,7 @@ describe CommitStatus, models: true do
       it { is_expected.to be_falsey }
     end
 
-    %w(running success failed).each do |status|
+    %w[running success failed].each do |status|
       context "if commit status is #{status}" do
         before { commit_status.status = status }
 
@@ -48,7 +52,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(pending canceled).each do |status|
+    %w[pending canceled].each do |status|
       context "if commit status is #{status}" do
         before { commit_status.status = status }
 
@@ -60,7 +64,7 @@ describe CommitStatus, models: true do
   describe '#active?' do
     subject { commit_status.active? }
 
-    %w(pending running).each do |state|
+    %w[pending running].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -68,7 +72,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(success failed canceled).each do |state|
+    %w[success failed canceled].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -80,7 +84,7 @@ describe CommitStatus, models: true do
   describe '#complete?' do
     subject { commit_status.complete? }
 
-    %w(success failed canceled).each do |state|
+    %w[success failed canceled].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -88,7 +92,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(pending running).each do |state|
+    %w[pending running].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -125,32 +129,53 @@ describe CommitStatus, models: true do
   describe '.latest' do
     subject { CommitStatus.latest.order(:id) }
 
-    before do
-      @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
-      @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
-      @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success'
-      @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success'
-      @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
+    let(:statuses) do
+      [create_status(name: 'aa', ref: 'bb', status: 'running'),
+       create_status(name: 'cc', ref: 'cc', status: 'pending'),
+       create_status(name: 'aa', ref: 'cc', status: 'success'),
+       create_status(name: 'cc', ref: 'bb', status: 'success'),
+       create_status(name: 'aa', ref: 'bb', status: 'success')]
     end
 
     it 'returns unique statuses' do
-      is_expected.to eq([@commit4, @commit5])
+      is_expected.to eq(statuses.values_at(3, 4))
     end
   end
 
   describe '.running_or_pending' do
     subject { CommitStatus.running_or_pending.order(:id) }
 
-    before do
-      @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
-      @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
-      @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success'
-      @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed'
-      @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
+    let(:statuses) do
+      [create_status(name: 'aa', ref: 'bb', status: 'running'),
+       create_status(name: 'cc', ref: 'cc', status: 'pending'),
+       create_status(name: 'aa', ref: nil, status: 'success'),
+       create_status(name: 'dd', ref: nil, status: 'failed'),
+       create_status(name: 'ee', ref: nil, status: 'canceled')]
     end
 
     it 'returns statuses that are running or pending' do
-      is_expected.to eq([@commit1, @commit2])
+      is_expected.to eq(statuses.values_at(0, 1))
+    end
+  end
+
+  describe '.exclude_ignored' do
+    subject { CommitStatus.exclude_ignored.order(:id) }
+
+    let(:statuses) do
+      [create_status(when: 'manual', status: 'skipped'),
+       create_status(when: 'manual', status: 'success'),
+       create_status(when: 'manual', status: 'failed'),
+       create_status(when: 'on_failure', status: 'skipped'),
+       create_status(when: 'on_failure', status: 'success'),
+       create_status(when: 'on_failure', status: 'failed'),
+       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')]
+    end
+
+    it 'returns statuses without what we want to ignore' do
+      is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9))
     end
   end
 
@@ -187,7 +212,7 @@ describe CommitStatus, models: true do
       subject { CommitStatus.where(pipeline: pipeline).stages }
 
       it 'returns ordered list of stages' do
-        is_expected.to eq(%w(build test deploy))
+        is_expected.to eq(%w[build test deploy])
       end
     end
 
@@ -223,4 +248,33 @@ describe CommitStatus, models: true do
       expect(commit_status.commit).to eq project.commit
     end
   end
+
+  describe '#group_name' do
+    subject { commit_status.group_name }
+
+    tests = {
+      'rspec:windows' => 'rspec:windows',
+      'rspec:windows 0' => 'rspec:windows 0',
+      'rspec:windows 0 test' => 'rspec:windows 0 test',
+      'rspec:windows 0 1' => 'rspec:windows',
+      'rspec:windows 0 1 name' => 'rspec:windows name',
+      'rspec:windows 0/1' => 'rspec:windows',
+      'rspec:windows 0/1 name' => 'rspec:windows name',
+      'rspec:windows 0:1' => 'rspec:windows',
+      'rspec:windows 0:1 name' => 'rspec:windows name',
+      'rspec:windows 10000 20000' => 'rspec:windows',
+      'rspec:windows 0 : / 1' => 'rspec:windows',
+      'rspec:windows 0 : / 1 name' => 'rspec:windows name',
+      '0 1 name ruby' => 'name ruby',
+      '0 :/ 1 name ruby' => 'name ruby'
+    }
+
+    tests.each do |name, group_name|
+      it "'#{name}' puts in '#{group_name}'" do
+        commit_status.name = name
+
+        is_expected.to eq(group_name)
+      end
+    end
+  end
 end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index a371c4a18a9255b4966978c234b1a0fb2e2ac255..de791abdf3dbc66b26059b37db419a925a61922b 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -45,4 +45,14 @@ describe Issue, "Awardable" do
       expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
     end
   end
+
+  describe 'querying award_emoji on an Awardable' do
+    let(:issue) { create(:issue) }
+
+    it 'sorts in ascending fashion' do
+      create_list(:award_emoji, 3, awardable: issue)
+
+      expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id)
+    end
+  end
 end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2e3702f7520c401a9094bba865db4cc481efef10
--- /dev/null
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+describe CacheMarkdownField do
+  CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+
+  # The minimum necessary ActiveModel to test this concern
+  class ThingWithMarkdownFields
+    include ActiveModel::Model
+    include ActiveModel::Dirty
+
+    include ActiveModel::Serialization
+
+    class_attribute :attribute_names
+    self.attribute_names = []
+
+    def attributes
+      attribute_names.each_with_object({}) do |name, hsh|
+        hsh[name.to_s] = send(name)
+      end
+    end
+
+    extend ActiveModel::Callbacks
+    define_model_callbacks :save
+
+    include CacheMarkdownField
+    cache_markdown_field :foo
+    cache_markdown_field :baz, pipeline: :single_line
+
+    def self.add_attr(attr_name)
+      self.attribute_names += [attr_name]
+      define_attribute_methods(attr_name)
+      attr_reader(attr_name)
+      define_method("#{attr_name}=") do |val|
+        send("#{attr_name}_will_change!") unless val == send(attr_name)
+        instance_variable_set("@#{attr_name}", val)
+      end
+    end
+
+    [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
+      add_attr(attr_name)
+    end
+
+    def initialize(*)
+      super
+
+      # Pretend new is load
+      clear_changes_information
+    end
+
+    def save
+      run_callbacks :save do
+        changes_applied
+      end
+    end
+  end
+
+  CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+
+  def thing_subclass(new_attr)
+    Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
+  end
+
+  let(:markdown) { "`Foo`" }
+  let(:html) { "<p><code>Foo</code></p>" }
+
+  let(:updated_markdown) { "`Bar`" }
+  let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
+
+  subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
+
+  describe ".attributes" do
+    it "excludes cache attributes" do
+      expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
+    end
+  end
+
+  describe ".cache_markdown_field" do
+    it "refuses to allow untracked classes" do
+      expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
+    end
+  end
+
+  context "an unchanged markdown field" do
+    before do
+      subject.foo = subject.foo
+      subject.save
+    end
+
+    it { expect(subject.foo).to eq(markdown) }
+    it { expect(subject.foo_html).to eq(html) }
+    it { expect(subject.foo_html_changed?).not_to be_truthy }
+  end
+
+  context "a changed markdown field" do
+    before do
+      subject.foo = updated_markdown
+      subject.save
+    end
+
+    it { expect(subject.foo_html).to eq(updated_html) }
+  end
+
+  context "a non-markdown field changed" do
+    before do
+      subject.bar = "OK"
+      subject.save
+    end
+
+    it { expect(subject.bar).to eq("OK") }
+    it { expect(subject.foo).to eq(markdown) }
+    it { expect(subject.foo_html).to eq(html) }
+  end
+
+  describe '#banzai_render_context' do
+    it "sets project to nil if the object lacks a project" do
+      context = subject.banzai_render_context(:foo)
+      expect(context).to have_key(:project)
+      expect(context[:project]).to be_nil
+    end
+
+    it "excludes author if the object lacks an author" do
+      context = subject.banzai_render_context(:foo)
+      expect(context).not_to have_key(:author)
+    end
+
+    it "raises if the context for an unrecognised field is requested" do
+      expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
+    end
+
+    it "includes the pipeline" do
+      context = subject.banzai_render_context(:baz)
+      expect(context[:pipeline]).to eq(:single_line)
+    end
+
+    it "returns copies of the context template" do
+      template = subject.cached_markdown_fields[:baz]
+      copy = subject.banzai_render_context(:baz)
+      expect(copy).not_to be(template)
+    end
+
+    context "with a project" do
+      subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
+
+      it "sets the project in the context" do
+        context = subject.banzai_render_context(:foo)
+        expect(context).to have_key(:project)
+        expect(context[:project]).to eq(:project)
+      end
+
+      it "invalidates the cache when project changes" do
+        subject.project = :new_project
+        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+        subject.save
+
+        expect(subject.foo_html).to eq(updated_html)
+        expect(subject.baz_html).to eq(updated_html)
+      end
+    end
+
+    context "with an author" do
+      subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
+
+      it "sets the author in the context" do
+        context = subject.banzai_render_context(:foo)
+        expect(context).to have_key(:author)
+        expect(context[:author]).to eq(:author)
+      end
+
+      it "invalidates the cache when author changes" do
+        subject.author = :new_author
+        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+        subject.save
+
+        expect(subject.foo_html).to eq(updated_html)
+        expect(subject.baz_html).to eq(updated_html)
+      end
+    end
+  end
+end
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f7b436f32e6bc3365753eae9824a0d83ca8032bc
--- /dev/null
+++ b/spec/models/concerns/expirable_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Expirable do
+  describe 'ProjectMember' do
+    let(:no_expire) { create(:project_member) }
+    let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) }
+    let(:expired) { create(:project_member, expires_at: Time.current - 6.days) }
+
+    describe '.expired' do
+      it { expect(ProjectMember.expired).to match_array([expired]) }
+    end
+
+    describe '#expired?' do
+      it { expect(no_expire.expired?).to eq(false) }
+      it { expect(expire_later.expired?).to eq(false) }
+      it { expect(expired.expired?).to eq(true) }
+    end
+
+    describe '#expires?' do
+      it { expect(no_expire.expires?).to eq(false) }
+      it { expect(expire_later.expires?).to eq(true) }
+      it { expect(expired.expires?).to eq(true) }
+    end
+
+    describe '#expires_soon?' do
+      it { expect(no_expire.expires_soon?).to eq(false) }
+      it { expect(expire_later.expires_soon?).to eq(true) }
+      it { expect(expired.expires_soon?).to eq(true) }
+    end
+  end
+end
diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb
similarity index 80%
rename from spec/models/concerns/statuseable_spec.rb
rename to spec/models/concerns/has_status_spec.rb
index 8e0a2a2cbdea9ab38280e8e439d64ce8661f7c2b..87bffbdc54e599d6fdbb694640e71358b277aa6c 100644
--- a/spec/models/concerns/statuseable_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,26 +1,17 @@
 require 'spec_helper'
 
-describe Statuseable do
-  before do
-    @object = Object.new
-    @object.extend(Statuseable::ClassMethods)
-  end
-
+describe HasStatus do
   describe '.status' do
-    before do
-      allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
-    end
+    subject { CommitStatus.status }
 
-    subject { @object.status }
-    
     shared_examples 'build status summary' do
       context 'all successful' do
-        let(:statuses) { Array.new(2) { create(type, status: :success) } }
+        let!(:statuses) { Array.new(2) { create(type, status: :success) } }
         it { is_expected.to eq 'success' }
       end
 
       context 'at least one failed' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success), create(type, status: :failed)]
         end
 
@@ -28,7 +19,7 @@ describe Statuseable do
       end
 
       context 'at least one running' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success), create(type, status: :running)]
         end
 
@@ -36,7 +27,7 @@ describe Statuseable do
       end
 
       context 'at least one pending' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success), create(type, status: :pending)]
         end
 
@@ -44,7 +35,7 @@ describe Statuseable do
       end
 
       context 'success and failed but allowed to fail' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success),
            create(type, status: :failed, allow_failure: true)]
         end
@@ -53,12 +44,15 @@ describe Statuseable do
       end
 
       context 'one failed but allowed to fail' do
-        let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+        let!(:statuses) do
+          [create(type, status: :failed, allow_failure: true)]
+        end
+
         it { is_expected.to eq 'success' }
       end
 
       context 'success and canceled' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success), create(type, status: :canceled)]
         end
 
@@ -66,7 +60,7 @@ describe Statuseable do
       end
 
       context 'one failed and one canceled' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :failed), create(type, status: :canceled)]
         end
 
@@ -74,7 +68,7 @@ describe Statuseable do
       end
 
       context 'one failed but allowed to fail and one canceled' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :failed, allow_failure: true),
            create(type, status: :canceled)]
         end
@@ -83,7 +77,7 @@ describe Statuseable do
       end
 
       context 'one running one canceled' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :running), create(type, status: :canceled)]
         end
 
@@ -91,14 +85,15 @@ describe Statuseable do
       end
 
       context 'all canceled' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :canceled), create(type, status: :canceled)]
         end
+
         it { is_expected.to eq 'canceled' }
       end
 
       context 'success and canceled but allowed to fail' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success),
            create(type, status: :canceled, allow_failure: true)]
         end
@@ -107,7 +102,7 @@ describe Statuseable do
       end
 
       context 'one finished and second running but allowed to fail' do
-        let(:statuses) do
+        let!(:statuses) do
           [create(type, status: :success),
            create(type, status: :running, allow_failure: true)]
         end
@@ -118,11 +113,13 @@ describe Statuseable do
 
     context 'ci build statuses' do
       let(:type) { :ci_build }
+
       it_behaves_like 'build status summary'
     end
 
     context 'generic commit statuses' do
       let(:type) { :generic_commit_status }
+
       it_behaves_like 'build status summary'
     end
   end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 60e4bbc85647025e44aafaf370a0f8315b8e37a3..6e987967ca5331243dd5bde1a31991370536f90e 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -97,6 +97,11 @@ describe Issue, "Issuable" do
     end
   end
 
+  describe '.to_ability_name' do
+    it { expect(Issue.to_ability_name).to eq("issue") }
+    it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
+  end
+
   describe "#today?" do
     it "returns true when created today" do
       # Avoid timezone differences and just return exactly what we want
@@ -298,6 +303,20 @@ describe Issue, "Issuable" do
     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) }
+
+    subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority }
+
+    before do
+      issue.labels << label_1
+      issue.labels << label_2
+    end
+
+    it { is_expected.to eq(2) }
+  end
+
   describe ".with_label" do
     let(:project) { create(:project, :public) }
     let(:bug) { create(:label, project: project, title: 'bug') }
@@ -327,4 +346,25 @@ describe Issue, "Issuable" do
       expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2])
     end
   end
+
+  describe '#assignee_or_author?' do
+    let(:user) { build(:user, id: 1) }
+    let(:issue) { build(:issue) }
+
+    it 'returns true for a user that is assigned to an issue' do
+      issue.assignee = user
+
+      expect(issue.assignee_or_author?(user)).to eq(true)
+    end
+
+    it 'returns true for a user that is the author of an issue' do
+      issue.author = user
+
+      expect(issue.assignee_or_author?(user)).to eq(true)
+    end
+
+    it 'returns false for a user that is not the assignee or author' do
+      expect(issue.assignee_or_author?(user)).to eq(false)
+    end
+  end
 end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 549b0042038333c13f0db2854c7036e8baf6e99d..132858950d54c5ac1464208c514c34ec6951597f 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -1,18 +1,27 @@
 require 'spec_helper'
 
 describe Mentionable do
-  include Mentionable
+  class Example
+    include Mentionable
 
-  def author
-    nil
+    attr_accessor :project, :message
+    attr_mentionable :message
+
+    def author
+      nil
+    end
   end
 
   describe 'references' do
     let(:project) { create(:project) }
+    let(:mentionable) { Example.new }
 
     it 'excludes JIRA references' do
       allow(project).to receive_messages(jira_tracker?: true)
-      expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
+
+      mentionable.project = project
+      mentionable.message = 'JIRA-123'
+      expect(mentionable.referenced_mentionables).to be_empty
     end
   end
 end
@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
       let(:user) { create(:user) }
 
       def referenced_issues(current_user)
-        text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
-
-        issue.referenced_mentionables(current_user, text)
+        issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
+        issue.referenced_mentionables(current_user)
       end
 
       context 'when the current user can see the issue' do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9041690023f0a7719ee0de805ed7e17652f256a4
--- /dev/null
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe ProjectFeaturesCompatibility do
+  let(:project) { create(:project) }
+  let(:features) { %w(issues wiki builds merge_requests snippets) }
+
+  # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
+  # All those fields got moved to a new table called project_feature and are now integers instead of booleans
+  # This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table
+  # So we can keep it compatible
+
+  it "converts fields from 'true' to ProjectFeature::ENABLED" do
+    features.each do |feature|
+      project.update_attribute("#{feature}_enabled".to_sym, "true")
+      expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
+    end
+  end
+
+  it "converts fields from 'false' to ProjectFeature::DISABLED" do
+    features.each do |feature|
+      project.update_attribute("#{feature}_enabled".to_sym, "false")
+      expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
+    end
+  end
+
+  it "converts fields from true to ProjectFeature::ENABLED" do
+    features.each do |feature|
+      project.update_attribute("#{feature}_enabled".to_sym, true)
+      expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
+    end
+  end
+
+  it "converts fields from false to ProjectFeature::DISABLED" do
+    features.each do |feature|
+      project.update_attribute("#{feature}_enabled".to_sym, false)
+      expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7691d690db0fc79dbfcb653a2641c905dcce8564
--- /dev/null
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#code', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  context 'with deployment' do
+    generate_cycle_analytics_spec(
+      phase: :code,
+      data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+      start_time_conditions: [["issue mentioned in a commit",
+                               -> (context, data) do
+                                 context.create_commit_referencing_issue(data[:issue])
+                               end]],
+      end_time_conditions:   [["merge request that closes issue is created",
+                               -> (context, data) do
+                                 context.create_merge_request_closing_issue(data[:issue])
+                               end]],
+      post_fn: -> (context, data) do
+        context.merge_merge_requests_closing_issue(data[:issue])
+        context.deploy_master
+      end)
+
+    context "when a regular merge request (that doesn't close the issue) is created" do
+      it "returns nil" do
+        5.times do
+          issue = create(:issue, project: project)
+
+          create_commit_referencing_issue(issue)
+          create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+          merge_merge_requests_closing_issue(issue)
+          deploy_master
+        end
+
+        expect(subject.code).to be_nil
+      end
+    end
+  end
+
+  context 'without deployment' do
+    generate_cycle_analytics_spec(
+      phase: :code,
+      data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+      start_time_conditions: [["issue mentioned in a commit",
+                               -> (context, data) do
+                                 context.create_commit_referencing_issue(data[:issue])
+                               end]],
+      end_time_conditions:   [["merge request that closes issue is created",
+                               -> (context, data) do
+                                 context.create_merge_request_closing_issue(data[:issue])
+                               end]],
+      post_fn: -> (context, data) do
+        context.merge_merge_requests_closing_issue(data[:issue])
+      end)
+
+    context "when a regular merge request (that doesn't close the issue) is created" do
+      it "returns nil" do
+        5.times do
+          issue = create(:issue, project: project)
+
+          create_commit_referencing_issue(issue)
+          create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+          merge_merge_requests_closing_issue(issue)
+        end
+
+        expect(subject.code).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f649b44d3670b4016be3c900ad859c3271bd10f9
--- /dev/null
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#issue', models: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :issue,
+    data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+    start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
+    end_time_conditions:   [["issue associated with a milestone",
+                             -> (context, data) do
+                               if data[:issue].persisted?
+                                 data[:issue].update(milestone: context.create(:milestone, project: context.project))
+                               end
+                             end],
+                            ["list label added to issue",
+                             -> (context, data) do
+                               if data[:issue].persisted?
+                                 data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+                               end
+                             end]],
+    post_fn: -> (context, data) do
+      if data[:issue].persisted?
+        context.create_merge_request_closing_issue(data[:issue].reload)
+        context.merge_merge_requests_closing_issue(data[:issue])
+      end
+    end)
+
+  context "when a regular label (instead of a list label) is added to the issue" do
+    it "returns nil" do
+      5.times do
+        regular_label = create(:label)
+        issue = create(:issue, project: project)
+        issue.update(label_ids: [regular_label.id])
+
+        create_merge_request_closing_issue(issue)
+        merge_merge_requests_closing_issue(issue)
+      end
+
+      expect(subject.issue).to be_nil
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2cdefbeef21bcd2d4441e38d48f5ee3ee34d8ebe
--- /dev/null
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#plan', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :plan,
+    data_fn: -> (context) do
+      {
+        issue: context.create(:issue, project: context.project),
+        branch_name: context.random_git_name
+      }
+    end,
+    start_time_conditions: [["issue associated with a milestone",
+                             -> (context, data) do
+                               data[:issue].update(milestone: context.create(:milestone, project: context.project))
+                             end],
+                            ["list label added to issue",
+                             -> (context, data) do
+                               data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+                             end]],
+    end_time_conditions:   [["issue mentioned in a commit",
+                             -> (context, data) do
+                               context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
+                             end]],
+    post_fn: -> (context, data) do
+      context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
+      context.merge_merge_requests_closing_issue(data[:issue])
+    end)
+
+  context "when a regular label (instead of a list label) is added to the issue" do
+    it "returns nil" do
+      branch_name = random_git_name
+      label = create(:label)
+      issue = create(:issue, project: project)
+      issue.update(label_ids: [label.id])
+      create_commit_referencing_issue(issue, branch_name: branch_name)
+
+      create_merge_request_closing_issue(issue, source_branch: branch_name)
+      merge_merge_requests_closing_issue(issue)
+
+      expect(subject.issue).to be_nil
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f5e5cab92d0698f105b721f1f796b4d2af54623
--- /dev/null
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#production', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :production,
+    data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+    start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
+    before_end_fn: lambda do |context, data|
+      context.create_merge_request_closing_issue(data[:issue])
+      context.merge_merge_requests_closing_issue(data[:issue])
+    end,
+    end_time_conditions:
+      [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
+       ["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(context.user, context.random_git_name, "content", "commit message", 'master', false)
+          context.project.repository.commit(sha)
+
+          context.deploy_master
+        end]])
+
+  context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+    it "returns nil" do
+      5.times do
+        merge_request = create(:merge_request)
+        MergeRequests::MergeService.new(project, user).execute(merge_request)
+        deploy_master
+      end
+
+      expect(subject.production).to be_nil
+    end
+  end
+
+  context "when the deployment happens to a non-production environment" do
+    it "returns nil" do
+      5.times do
+        issue = create(:issue, project: project)
+        merge_request = create_merge_request_closing_issue(issue)
+        MergeRequests::MergeService.new(project, user).execute(merge_request)
+        deploy_master(environment: 'staging')
+      end
+
+      expect(subject.production).to be_nil
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0ed080a42b1d7ff094b25259f66075d1361fff14
--- /dev/null
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#review', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :review,
+    data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+    start_time_conditions: [["merge request that closes issue is created",
+                             -> (context, data) do
+                               context.create_merge_request_closing_issue(data[:issue])
+                             end]],
+    end_time_conditions:   [["merge request that closes issue is merged",
+                             -> (context, data) do
+                               context.merge_merge_requests_closing_issue(data[:issue])
+                             end]],
+    post_fn: nil)
+
+  context "when a regular merge request (that doesn't close the issue) is created and merged" do
+    it "returns nil" do
+      5.times do
+        MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
+      end
+
+      expect(subject.review).to be_nil
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af1c4477ddb58bdd9f7ba2dfccdc3961362ccc19
--- /dev/null
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#staging', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :staging,
+    data_fn: lambda do |context|
+      issue = context.create(:issue, project: context.project)
+      { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
+    end,
+    start_time_conditions: [["merge request that closes issue is merged",
+                             -> (context, data) do
+                               context.merge_merge_requests_closing_issue(data[:issue])
+                             end ]],
+    end_time_conditions:   [["merge request that closes issue is deployed to production",
+                             -> (context, data) do
+                               context.deploy_master
+                             end],
+                            ["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(
+                                 context.user,
+                                 context.random_git_name,
+                                 "content",
+                                 "commit message",
+                                 'master',
+                                 false)
+                               context.project.repository.commit(sha)
+
+                               context.deploy_master
+                             end]])
+
+  context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+    it "returns nil" do
+      5.times do
+        merge_request = create(:merge_request)
+        MergeRequests::MergeService.new(project, user).execute(merge_request)
+        deploy_master
+      end
+
+      expect(subject.staging).to be_nil
+    end
+  end
+
+  context "when the deployment happens to a non-production environment" do
+    it "returns nil" do
+      5.times do
+        issue = create(:issue, project: project)
+        merge_request = create_merge_request_closing_issue(issue)
+        MergeRequests::MergeService.new(project, user).execute(merge_request)
+        deploy_master(environment: 'staging')
+      end
+
+      expect(subject.staging).to be_nil
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9d67bc82cbaf5703727c76d165181782432beadc
--- /dev/null
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe CycleAnalytics::Summary, models: true do
+  let(:project) { create(:project) }
+  let(:from) { Time.now }
+  let(:user) { create(:user, :admin) }
+  subject { described_class.new(project, from: from) }
+
+  describe "#new_issues" do
+    it "finds the number of issues created after the 'from date'" do
+      Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+      Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+
+      expect(subject.new_issues).to eq(1)
+    end
+
+    it "doesn't find issues from other projects" do
+      Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
+
+      expect(subject.new_issues).to eq(0)
+    end
+  end
+
+  describe "#commits" do
+    it "finds the number of commits created after the 'from date'" do
+      Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+      Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+
+      expect(subject.commits).to eq(1)
+    end
+
+    it "doesn't find commits from other projects" do
+      Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
+
+      expect(subject.commits).to eq(0)
+    end
+
+    it "finds a large (> 100) snumber of commits if present" do
+      Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
+
+      expect(subject.commits).to eq(100)
+    end
+  end
+
+  describe "#deploys" do
+    it "finds the number of deploys made created after the 'from date'" do
+      Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
+      Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+
+      expect(subject.deploys).to eq(1)
+    end
+
+    it "doesn't find commits from other projects" do
+      Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
+
+      expect(subject.deploys).to eq(0)
+    end
+  end
+end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02ddfeed9c1695cc5208435fbeadebca907ed824
--- /dev/null
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#test', feature: true do
+  extend CycleAnalyticsHelpers::TestGeneration
+
+  let(:project) { create(:project) }
+  let(:from_date) { 10.days.ago }
+  let(:user) { create(:user, :admin) }
+  subject { CycleAnalytics.new(project, from: from_date) }
+
+  generate_cycle_analytics_spec(
+    phase: :test,
+    data_fn: lambda do |context|
+      issue = context.create(:issue, project: context.project)
+      merge_request = context.create_merge_request_closing_issue(issue)
+      pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+      { pipeline: pipeline, issue: issue }
+    end,
+    start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
+    end_time_conditions:   [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
+    post_fn: -> (context, data) do
+      context.merge_merge_requests_closing_issue(data[:issue])
+    end)
+
+  context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
+    it "returns nil" do
+      5.times do
+        issue = create(:issue, project: project)
+        merge_request = create_merge_request_closing_issue(issue)
+        pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+        pipeline.run!
+        pipeline.succeed!
+
+        merge_merge_requests_closing_issue(issue)
+      end
+
+      expect(subject.test).to be_nil
+    end
+  end
+
+  context "when the pipeline is not for a merge request" do
+    it "returns nil" do
+      5.times do
+        pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+
+        pipeline.run!
+        pipeline.succeed!
+      end
+
+      expect(subject.test).to be_nil
+    end
+  end
+
+  context "when the pipeline is dropped (failed)" do
+    it "returns nil" do
+      5.times do
+        issue = create(:issue, project: project)
+        merge_request = create_merge_request_closing_issue(issue)
+        pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+        pipeline.run!
+        pipeline.drop!
+
+        merge_merge_requests_closing_issue(issue)
+      end
+
+      expect(subject.test).to be_nil
+    end
+  end
+
+  context "when the pipeline is cancelled" do
+    it "returns nil" do
+      5.times do
+        issue = create(:issue, project: project)
+        merge_request = create_merge_request_closing_issue(issue)
+        pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+        pipeline.run!
+        pipeline.cancel!
+
+        merge_merge_requests_closing_issue(issue)
+      end
+
+      expect(subject.test).to be_nil
+    end
+  end
+end
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 6a90598a629d3c6ad7ced553047926d29e30c1d3..93623e8e99b851581eab10537e3c91e63fd7f24d 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -1,9 +1,6 @@
 require 'spec_helper'
 
 describe DeployKey, models: true do
-  let(:project) { create(:project) }
-  let(:deploy_key) { create(:deploy_key, projects: [project]) }
-
   describe "Associations" do
     it { is_expected.to have_many(:deploy_keys_projects) }
     it { is_expected.to have_many(:projects) }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index bfff639ad78c55961c1707caec1649a48768b0f0..ca594a320c0772366241085429dba70672d02308 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -38,5 +38,60 @@ describe Deployment, models: true do
         expect(deployment.includes_commit?(commit)).to be true
       end
     end
+
+    context 'when the SHA for the deployment does not exist in the repo' do
+      it 'returns false' do
+        deployment.update(sha: Gitlab::Git::BLANK_SHA)
+        commit = project.commit
+
+        expect(deployment.includes_commit?(commit)).to be false
+      end
+    end
+  end
+
+  describe '#stop_action' do
+    let(:build) { create(:ci_build) }
+
+    subject { deployment.stop_action }
+
+    context 'when no other actions' do
+      let(:deployment) { FactoryGirl.build(:deployment, deployable: build) }
+
+      it { is_expected.to be_nil }
+    end
+
+    context 'with other actions' do
+      let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+      context 'when matching action is defined' do
+        let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
+
+        it { is_expected.to be_nil }
+      end
+
+      context 'when no matching action is defined' do
+        let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+
+        it { is_expected.to eq(close_action) }
+      end
+    end
+  end
+
+  describe '#stoppable?' do
+    subject { deployment.stoppable? }
+
+    context 'when no other actions' do
+      let(:deployment) { build(:deployment) }
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'when matching action is defined' do
+      let(:build) { create(:ci_build) }
+      let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+      let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+      it { is_expected.to be_truthy }
+    end
   end
 end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 1fa96eb1f158cf5653fe1b96acc48c0e6712d647..3db5937a4f3932eec5c2354fbb372e79ebe7fb33 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,6 +31,43 @@ describe DiffNote, models: true do
 
   subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
 
+  describe ".resolve!" do
+    let(:current_user) { create(:user) }
+    let!(:commit_note) { create(:diff_note_on_commit) }
+    let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+    let!(:unresolved_note) { create(:diff_note_on_merge_request) }
+
+    before do
+      described_class.resolve!(current_user)
+
+      commit_note.reload
+      resolved_note.reload
+      unresolved_note.reload
+    end
+
+    it 'resolves only the resolvable, not yet resolved notes' do
+      expect(commit_note.resolved_at).to be_nil
+      expect(resolved_note.resolved_by).not_to eq(current_user)
+      expect(unresolved_note.resolved_at).not_to be_nil
+      expect(unresolved_note.resolved_by).to eq(current_user)
+    end
+  end
+
+  describe ".unresolve!" do
+    let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+
+    before do
+      described_class.unresolve!
+
+      resolved_note.reload
+    end
+
+    it 'unresolves the resolved notes' do
+      expect(resolved_note.resolved_by).to be_nil
+      expect(resolved_note.resolved_at).to be_nil
+    end
+  end
+
   describe "#position=" do
     context "when provided a string" do
       it "sets the position" do
@@ -103,7 +140,7 @@ describe DiffNote, models: true do
 
   describe "#active?" do
     context "when noteable is a commit" do
-      subject { create(:diff_note_on_commit, project: project, position: position) }
+      subject { build(:diff_note_on_commit, project: project, position: position) }
 
       it "returns true" do
         expect(subject.active?).to be true
@@ -188,4 +225,300 @@ describe DiffNote, models: true do
       end
     end
   end
+
+  describe "#resolvable?" do
+    context "when noteable is a commit" do
+      subject { create(:diff_note_on_commit, project: project, position: position) }
+
+      it "returns false" do
+        expect(subject.resolvable?).to be false
+      end
+    end
+
+    context "when noteable is a merge request" do
+      context "when a system note" do
+        before do
+          subject.system = true
+        end
+
+        it "returns false" do
+          expect(subject.resolvable?).to be false
+        end
+      end
+
+      context "when a regular note" do
+        it "returns true" do
+          expect(subject.resolvable?).to be true
+        end
+      end
+    end
+  end
+
+  describe "#to_be_resolved?" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.to_be_resolved?).to be false
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when resolved" do
+        before do
+          allow(subject).to receive(:resolved?).and_return(true)
+        end
+
+        it "returns false" do
+          expect(subject.to_be_resolved?).to be false
+        end
+      end
+
+      context "when not resolved" do
+        before do
+          allow(subject).to receive(:resolved?).and_return(false)
+        end
+
+        it "returns true" do
+          expect(subject.to_be_resolved?).to be true
+        end
+      end
+    end
+  end
+
+  describe "#resolve!" do
+    let(:current_user) { create(:user) }
+
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns nil" do
+        expect(subject.resolve!(current_user)).to be_nil
+      end
+
+      it "doesn't set resolved_at" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved_at).to be_nil
+      end
+
+      it "doesn't set resolved_by" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved_by).to be_nil
+      end
+
+      it "doesn't mark as resolved" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved?).to be false
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when already resolved" do
+        let(:user) { create(:user) }
+
+        before do
+          subject.resolve!(user)
+        end
+
+        it "returns nil" do
+          expect(subject.resolve!(current_user)).to be_nil
+        end
+
+        it "doesn't change resolved_at" do
+          expect(subject.resolved_at).not_to be_nil
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+        end
+
+        it "doesn't change resolved_by" do
+          expect(subject.resolved_by).to eq(user)
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+        end
+
+        it "doesn't change resolved status" do
+          expect(subject.resolved?).to be true
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+        end
+      end
+
+      context "when not yet resolved" do
+        it "returns true" do
+          expect(subject.resolve!(current_user)).to be true
+        end
+
+        it "sets resolved_at" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved_by).to eq(current_user)
+        end
+
+        it "marks as resolved" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved?).to be true
+        end
+      end
+    end
+  end
+
+  describe "#unresolve!" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns nil" do
+        expect(subject.unresolve!).to be_nil
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when resolved" do
+        let(:user) { create(:user) }
+
+        before do
+          subject.resolve!(user)
+        end
+
+        it "returns true" do
+          expect(subject.unresolve!).to be true
+        end
+
+        it "unsets resolved_at" do
+          subject.unresolve!
+
+          expect(subject.resolved_at).to be_nil
+        end
+
+        it "unsets resolved_by" do
+          subject.unresolve!
+
+          expect(subject.resolved_by).to be_nil
+        end
+
+        it "unmarks as resolved" do
+          subject.unresolve!
+
+          expect(subject.resolved?).to be false
+        end
+      end
+
+      context "when not resolved" do
+        it "returns nil" do
+          expect(subject.unresolve!).to be_nil
+        end
+      end
+    end
+  end
+
+  describe "#discussion" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns nil" do
+        expect(subject.discussion).to be_nil
+      end
+    end
+
+    context "when resolvable" do
+      let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
+      let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+
+      let(:active_position2) do
+        Gitlab::Diff::Position.new(
+          old_path: "files/ruby/popen.rb",
+          new_path: "files/ruby/popen.rb",
+          old_line: 16,
+          new_line: 22,
+          diff_refs: merge_request.diff_refs
+        )
+      end
+
+      it "returns the discussion this note is in" do
+        discussion = subject.discussion
+
+        expect(discussion.id).to eq(subject.discussion_id)
+        expect(discussion.notes).to eq([subject, diff_note2])
+      end
+    end
+  end
+
+  describe "#discussion_id" do
+    let(:note) { create(:diff_note_on_merge_request) }
+
+    context "when it is newly created" do
+      it "has a discussion id" do
+        expect(note.discussion_id).not_to be_nil
+        expect(note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+
+    context "when it didn't store a discussion id before" do
+      before do
+        note.update_column(:discussion_id, nil)
+      end
+
+      it "has a discussion id" do
+        # The discussion_id is set in `after_initialize`, so `reload` won't work
+        reloaded_note = Note.find(note.id)
+
+        expect(reloaded_note.discussion_id).not_to be_nil
+        expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+  end
+
+  describe "#original_discussion_id" do
+    let(:note) { create(:diff_note_on_merge_request) }
+
+    context "when it is newly created" do
+      it "has a discussion id" do
+        expect(note.original_discussion_id).not_to be_nil
+        expect(note.original_discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+
+    context "when it didn't store a discussion id before" do
+      before do
+        note.update_column(:original_discussion_id, nil)
+      end
+
+      it "has a discussion id" do
+        # The original_discussion_id is set in `after_initialize`, so `reload` won't work
+        reloaded_note = Note.find(note.id)
+
+        expect(reloaded_note.original_discussion_id).not_to be_nil
+        expect(reloaded_note.original_discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+  end
 end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0142706d140039b54b98fa3e3e1e713713bfac3e
--- /dev/null
+++ b/spec/models/discussion_spec.rb
@@ -0,0 +1,593 @@
+require 'spec_helper'
+
+describe Discussion, model: true do
+  subject { described_class.new([first_note, second_note, third_note]) }
+
+  let(:first_note) { create(:diff_note_on_merge_request) }
+  let(:second_note) { create(:diff_note_on_merge_request) }
+  let(:third_note) { create(:diff_note_on_merge_request) }
+
+  describe "#resolvable?" do
+    context "when a diff discussion" do
+      before do
+        allow(subject).to receive(:diff_discussion?).and_return(true)
+      end
+
+      context "when all notes are unresolvable" do
+        before do
+          allow(first_note).to receive(:resolvable?).and_return(false)
+          allow(second_note).to receive(:resolvable?).and_return(false)
+          allow(third_note).to receive(:resolvable?).and_return(false)
+        end
+
+        it "returns false" do
+          expect(subject.resolvable?).to be false
+        end
+      end
+
+      context "when some notes are unresolvable and some notes are resolvable" do
+        before do
+          allow(first_note).to receive(:resolvable?).and_return(true)
+          allow(second_note).to receive(:resolvable?).and_return(false)
+          allow(third_note).to receive(:resolvable?).and_return(true)
+        end
+
+        it "returns true" do
+          expect(subject.resolvable?).to be true
+        end
+      end
+
+      context "when all notes are resolvable" do
+        before do
+          allow(first_note).to receive(:resolvable?).and_return(true)
+          allow(second_note).to receive(:resolvable?).and_return(true)
+          allow(third_note).to receive(:resolvable?).and_return(true)
+        end
+
+        it "returns true" do
+          expect(subject.resolvable?).to be true
+        end
+      end
+    end
+
+    context "when not a diff discussion" do
+      before do
+        allow(subject).to receive(:diff_discussion?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.resolvable?).to be false
+      end
+    end
+  end
+
+  describe "#resolved?" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.resolved?).to be false
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+
+        allow(first_note).to receive(:resolvable?).and_return(true)
+        allow(second_note).to receive(:resolvable?).and_return(false)
+        allow(third_note).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when all resolvable notes are resolved" do
+        before do
+          allow(first_note).to receive(:resolved?).and_return(true)
+          allow(third_note).to receive(:resolved?).and_return(true)
+        end
+
+        it "returns true" do
+          expect(subject.resolved?).to be true
+        end
+      end
+
+      context "when some resolvable notes are not resolved" do
+        before do
+          allow(first_note).to receive(:resolved?).and_return(true)
+          allow(third_note).to receive(:resolved?).and_return(false)
+        end
+
+        it "returns false" do
+          expect(subject.resolved?).to be false
+        end
+      end
+    end
+  end
+
+  describe "#to_be_resolved?" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.to_be_resolved?).to be false
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+
+        allow(first_note).to receive(:resolvable?).and_return(true)
+        allow(second_note).to receive(:resolvable?).and_return(false)
+        allow(third_note).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when all resolvable notes are resolved" do
+        before do
+          allow(first_note).to receive(:resolved?).and_return(true)
+          allow(third_note).to receive(:resolved?).and_return(true)
+        end
+
+        it "returns false" do
+          expect(subject.to_be_resolved?).to be false
+        end
+      end
+
+      context "when some resolvable notes are not resolved" do
+        before do
+          allow(first_note).to receive(:resolved?).and_return(true)
+          allow(third_note).to receive(:resolved?).and_return(false)
+        end
+
+        it "returns true" do
+          expect(subject.to_be_resolved?).to be true
+        end
+      end
+    end
+  end
+
+  describe "#can_resolve?" do
+    let(:current_user) { create(:user) }
+
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.can_resolve?(current_user)).to be false
+      end
+    end
+
+    context "when resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when not signed in" do
+        let(:current_user) { nil }
+
+        it "returns false" do
+          expect(subject.can_resolve?(current_user)).to be false
+        end
+      end
+
+      context "when signed in" do
+        context "when the signed in user is the noteable author" do
+          before do
+            subject.noteable.author = current_user
+          end
+
+          it "returns true" do
+            expect(subject.can_resolve?(current_user)).to be true
+          end
+        end
+
+        context "when the signed in user can push to the project" do
+          before do
+            subject.project.team << [current_user, :master]
+          end
+
+          it "returns true" do
+            expect(subject.can_resolve?(current_user)).to be true
+          end
+        end
+
+        context "when the signed in user is a random user" do
+          it "returns false" do
+            expect(subject.can_resolve?(current_user)).to be false
+          end
+        end
+      end
+    end
+  end
+
+  describe "#resolve!" do
+    let(:current_user) { create(:user) }
+
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns nil" do
+        expect(subject.resolve!(current_user)).to be_nil
+      end
+
+      it "doesn't set resolved_at" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved_at).to be_nil
+      end
+
+      it "doesn't set resolved_by" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved_by).to be_nil
+      end
+
+      it "doesn't mark as resolved" do
+        subject.resolve!(current_user)
+
+        expect(subject.resolved?).to be false
+      end
+    end
+
+    context "when resolvable" do
+      let(:user) { create(:user) }
+      let(:second_note) { create(:diff_note_on_commit) } # unresolvable
+
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when all resolvable notes are resolved" do
+        before do
+          first_note.resolve!(user)
+          third_note.resolve!(user)
+
+          first_note.reload
+          third_note.reload
+        end
+
+        it "doesn't change resolved_at on the resolved notes" do
+          expect(first_note.resolved_at).not_to be_nil
+          expect(third_note.resolved_at).not_to be_nil
+
+          expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+          expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
+        end
+
+        it "doesn't change resolved_by on the resolved notes" do
+          expect(first_note.resolved_by).to eq(user)
+          expect(third_note.resolved_by).to eq(user)
+
+          expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+          expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
+        end
+
+        it "doesn't change the resolved state on the resolved notes" do
+          expect(first_note.resolved?).to be true
+          expect(third_note.resolved?).to be true
+
+          expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+          expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
+        end
+
+        it "doesn't change resolved_at" do
+          expect(subject.resolved_at).not_to be_nil
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+        end
+
+        it "doesn't change resolved_by" do
+          expect(subject.resolved_by).to eq(user)
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+        end
+
+        it "doesn't change resolved state" do
+          expect(subject.resolved?).to be true
+
+          expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+        end
+      end
+
+      context "when some resolvable notes are resolved" do
+        before do
+          first_note.resolve!(user)
+        end
+
+        it "doesn't change resolved_at on the resolved note" do
+          expect(first_note.resolved_at).not_to be_nil
+
+          expect { subject.resolve!(current_user) }.
+            not_to change { first_note.reload.resolved_at }
+        end
+
+        it "doesn't change resolved_by on the resolved note" do
+          expect(first_note.resolved_by).to eq(user)
+
+          expect { subject.resolve!(current_user) }.
+            not_to change { first_note.reload && first_note.resolved_by }
+        end
+
+        it "doesn't change the resolved state on the resolved note" do
+          expect(first_note.resolved?).to be true
+
+          expect { subject.resolve!(current_user) }.
+            not_to change { first_note.reload && first_note.resolved? }
+        end
+
+        it "sets resolved_at on the unresolved note" do
+          subject.resolve!(current_user)
+          third_note.reload
+
+          expect(third_note.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by on the unresolved note" do
+          subject.resolve!(current_user)
+          third_note.reload
+
+          expect(third_note.resolved_by).to eq(current_user)
+        end
+
+        it "marks the unresolved note as resolved" do
+          subject.resolve!(current_user)
+          third_note.reload
+
+          expect(third_note.resolved?).to be true
+        end
+
+        it "sets resolved_at" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved_by).to eq(current_user)
+        end
+
+        it "marks as resolved" do
+          subject.resolve!(current_user)
+
+          expect(subject.resolved?).to be true
+        end
+      end
+
+      context "when no resolvable notes are resolved" do
+        it "sets resolved_at on the unresolved notes" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved_at).not_to be_nil
+          expect(third_note.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by on the unresolved notes" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved_by).to eq(current_user)
+          expect(third_note.resolved_by).to eq(current_user)
+        end
+
+        it "marks the unresolved notes as resolved" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved?).to be true
+          expect(third_note.resolved?).to be true
+        end
+
+        it "sets resolved_at" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(subject.resolved_at).not_to be_nil
+        end
+
+        it "sets resolved_by" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(subject.resolved_by).to eq(current_user)
+        end
+
+        it "marks as resolved" do
+          subject.resolve!(current_user)
+          first_note.reload
+          third_note.reload
+
+          expect(subject.resolved?).to be true
+        end
+      end
+    end
+  end
+
+  describe "#unresolve!" do
+    context "when not resolvable" do
+      before do
+        allow(subject).to receive(:resolvable?).and_return(false)
+      end
+
+      it "returns nil" do
+        expect(subject.unresolve!).to be_nil
+      end
+    end
+
+    context "when resolvable" do
+      let(:user) { create(:user) }
+
+      before do
+        allow(subject).to receive(:resolvable?).and_return(true)
+
+        allow(first_note).to receive(:resolvable?).and_return(true)
+        allow(second_note).to receive(:resolvable?).and_return(false)
+        allow(third_note).to receive(:resolvable?).and_return(true)
+      end
+
+      context "when all resolvable notes are resolved" do
+        before do
+          first_note.resolve!(user)
+          third_note.resolve!(user)
+        end
+
+        it "unsets resolved_at on the resolved notes" do
+          subject.unresolve!
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved_at).to be_nil
+          expect(third_note.resolved_at).to be_nil
+        end
+
+        it "unsets resolved_by on the resolved notes" do
+          subject.unresolve!
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved_by).to be_nil
+          expect(third_note.resolved_by).to be_nil
+        end
+
+        it "unmarks the resolved notes as resolved" do
+          subject.unresolve!
+          first_note.reload
+          third_note.reload
+
+          expect(first_note.resolved?).to be false
+          expect(third_note.resolved?).to be false
+        end
+
+        it "unsets resolved_at" do
+          subject.unresolve!
+          first_note.reload
+          third_note.reload
+
+          expect(subject.resolved_at).to be_nil
+        end
+
+        it "unsets resolved_by" do
+          subject.unresolve!
+          first_note.reload
+          third_note.reload
+
+          expect(subject.resolved_by).to be_nil
+        end
+
+        it "unmarks as resolved" do
+          subject.unresolve!
+
+          expect(subject.resolved?).to be false
+        end
+      end
+
+      context "when some resolvable notes are resolved" do
+        before do
+          first_note.resolve!(user)
+        end
+
+        it "unsets resolved_at on the resolved note" do
+          subject.unresolve!
+
+          expect(subject.first_note.resolved_at).to be_nil
+        end
+
+        it "unsets resolved_by on the resolved note" do
+          subject.unresolve!
+
+          expect(subject.first_note.resolved_by).to be_nil
+        end
+
+        it "unmarks the resolved note as resolved" do
+          subject.unresolve!
+
+          expect(subject.first_note.resolved?).to be false
+        end
+      end
+    end
+  end
+
+  describe "#collapsed?" do
+    context "when a diff discussion" do
+      before do
+        allow(subject).to receive(:diff_discussion?).and_return(true)
+      end
+
+      context "when resolvable" do
+        before do
+          allow(subject).to receive(:resolvable?).and_return(true)
+        end
+
+        context "when resolved" do
+          before do
+            allow(subject).to receive(:resolved?).and_return(true)
+          end
+
+          it "returns true" do
+            expect(subject.collapsed?).to be true
+          end
+        end
+
+        context "when not resolved" do
+          before do
+            allow(subject).to receive(:resolved?).and_return(false)
+          end
+
+          it "returns false" do
+            expect(subject.collapsed?).to be false
+          end
+        end
+      end
+
+      context "when not resolvable" do
+        before do
+          allow(subject).to receive(:resolvable?).and_return(false)
+        end
+
+        context "when active" do
+          before do
+            allow(subject).to receive(:active?).and_return(true)
+          end
+
+          it "returns false" do
+            expect(subject.collapsed?).to be false
+          end
+        end
+
+        context "when outdated" do
+          before do
+            allow(subject).to receive(:active?).and_return(false)
+          end
+
+          it "returns true" do
+            expect(subject.collapsed?).to be true
+          end
+        end
+      end
+    end
+
+    context "when not a diff discussion" do
+      before do
+        allow(subject).to receive(:diff_discussion?).and_return(false)
+      end
+
+      it "returns false" do
+        expect(subject.collapsed?).to be false
+      end
+    end
+  end
+end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index d9df9e0f9071223af1980433f374b5c400d93565..fe4de1b2afb162b8e655f0b2e0fb59d558447ba4 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -6,4 +6,9 @@ describe Email, models: true do
       subject { build(:email) }
     end
   end
+
+  it 'normalize email value' do
+    expect(described_class.new(email: ' inFO@exAMPLe.com ').email)
+      .to eq 'info@example.com'
+  end
 end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c881897926e36b4172dc977fa8dc3fec10f97c71..a94e6d0165fb4557826df0b6423e0240172982c2 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -8,6 +8,8 @@ describe Environment, models: true do
 
   it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
 
+  it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
+
   it { is_expected.to validate_presence_of(:name) }
   it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
   it { is_expected.to validate_length_of(:name).is_within(0..255) }
@@ -63,4 +65,105 @@ describe Environment, models: true do
       end
     end
   end
+
+  describe '#first_deployment_for' do
+    let(:project)       { create(:project) }
+    let!(:environment)  { create(:environment, project: project) }
+    let!(:deployment)   { create(:deployment, environment: environment, ref: commit.parent.id) }
+    let!(:deployment1)  { create(:deployment, environment: environment, ref: commit.id) }
+    let(:head_commit)   { project.commit }
+    let(:commit)        { project.commit.parent }
+
+    it 'returns deployment id for the environment' do
+      expect(environment.first_deployment_for(commit)).to eq deployment1
+    end
+
+    it 'return nil when no deployment is found' do
+      expect(environment.first_deployment_for(head_commit)).to eq nil
+    end
+  end
+
+  describe '#environment_type' do
+    subject { environment.environment_type }
+
+    it 'sets a environment type if name has multiple segments' do
+      environment.update!(name: 'production/worker.gitlab.com')
+
+      is_expected.to eq('production')
+    end
+
+    it 'nullifies a type if it\'s a simple name' do
+      environment.update!(name: 'production')
+
+      is_expected.to be_nil
+    end
+  end
+
+  describe '#stoppable?' do
+    subject { environment.stoppable? }
+
+    context 'when no other actions' do
+      it { is_expected.to be_falsey }
+    end
+
+    context 'when matching action is defined' do
+      let(:build) { create(:ci_build) }
+      let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+      let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+      context 'when environment is available' do
+        before do
+          environment.start
+        end
+
+        it { is_expected.to be_truthy }
+      end
+
+      context 'when environment is stopped' do
+        before do
+          environment.stop
+        end
+
+        it { is_expected.to be_falsey }
+      end
+    end
+  end
+
+  describe '#stop!' do
+    let(:user) { create(:user) }
+
+    subject { environment.stop!(user) }
+
+    before do
+      expect(environment).to receive(:stoppable?).and_call_original
+    end
+
+    context 'when no other actions' do
+      it { is_expected.to be_nil }
+    end
+
+    context 'when matching action is defined' do
+      let(:build) { create(:ci_build) }
+      let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+      context 'when action did not yet finish' do
+        let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
+
+        it 'returns the same action' do
+          expect(subject).to eq(close_action)
+          expect(subject.user).to eq(user)
+        end
+      end
+
+      context 'if action did finish' do
+        let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
+
+        it 'returns a new action of the same type' do
+          is_expected.to be_persisted
+          expect(subject.name).to eq(close_action.name)
+          expect(subject.user).to eq(user)
+        end
+      end
+    end
+  end
 end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b5d0d79e14e836b3652a7d4c5bfa63be9c6b557f..29a3af68a9bada769ae77806f7bd12e3c7c3cff1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -16,34 +16,56 @@ describe Event, models: true do
 
   describe 'Callbacks' do
     describe 'after_create :reset_project_activity' do
-      let(:project) { create(:project) }
+      let(:project) { create(:empty_project) }
 
-      context "project's last activity was less than 5 minutes ago" do
-        it 'does not update project.last_activity_at if it has been touched less than 5 minutes ago' do
-          create_event(project, project.owner)
-          project.update_column(:last_activity_at, 5.minutes.ago)
-          project_last_activity_at = project.last_activity_at
+      it 'calls the reset_project_activity method' do
+        expect_any_instance_of(Event).to receive(:reset_project_activity)
 
-          create_event(project, project.owner)
-
-          expect(project.last_activity_at).to eq(project_last_activity_at)
-        end
+        create_event(project, project.owner)
       end
     end
   end
 
   describe "Push event" do
-    before do
-      project = create(:project)
-      @user = project.owner
-      @event = create_event(project, @user)
+    let(:project) { create(:project, :private) }
+    let(:user) { project.owner }
+    let(:event) { create_event(project, user) }
+
+    it do
+      expect(event.push?).to be_truthy
+      expect(event.visible_to_user?(user)).to be_truthy
+      expect(event.visible_to_user?(nil)).to be_falsey
+      expect(event.tag?).to be_falsey
+      expect(event.branch_name).to eq("master")
+      expect(event.author).to eq(user)
+    end
+  end
+
+  describe '#membership_changed?' do
+    context "created" do
+      subject { build(:event, action: Event::CREATED).membership_changed? }
+      it { is_expected.to be_falsey }
+    end
+
+    context "updated" do
+      subject { build(:event, action: Event::UPDATED).membership_changed? }
+      it { is_expected.to be_falsey }
+    end
+
+    context "expired" do
+      subject { build(:event, action: Event::EXPIRED).membership_changed? }
+      it { is_expected.to be_truthy }
+    end
+
+    context "left" do
+      subject { build(:event, action: Event::LEFT).membership_changed? }
+      it { is_expected.to be_truthy }
     end
 
-    it { expect(@event.push?).to be_truthy }
-    it { expect(@event.visible_to_user?).to be_truthy }
-    it { expect(@event.tag?).to be_falsey }
-    it { expect(@event.branch_name).to eq("master") }
-    it { expect(@event.author).to eq(@user) }
+    context "joined" do
+      subject { build(:event, action: Event::JOINED).membership_changed? }
+      it { is_expected.to be_truthy }
+    end
   end
 
   describe '#note?' do
@@ -65,8 +87,8 @@ describe Event, models: true do
   describe '#visible_to_user?' do
     let(:project) { create(:empty_project, :public) }
     let(:non_member) { create(:user) }
-    let(:member)  { create(:user) }
-    let(:guest)  { create(:user) }
+    let(:member) { create(:user) }
+    let(:guest) { create(:user) }
     let(:author) { create(:author) }
     let(:assignee) { create(:user) }
     let(:admin) { create(:admin) }
@@ -85,23 +107,27 @@ describe Event, models: true do
       context 'for non confidential issues' do
         let(:target) { issue }
 
-        it { expect(event.visible_to_user?(non_member)).to eq true }
-        it { expect(event.visible_to_user?(author)).to eq true }
-        it { expect(event.visible_to_user?(assignee)).to eq true }
-        it { expect(event.visible_to_user?(member)).to eq true }
-        it { expect(event.visible_to_user?(guest)).to eq true }
-        it { expect(event.visible_to_user?(admin)).to eq true }
+        it do
+          expect(event.visible_to_user?(non_member)).to eq true
+          expect(event.visible_to_user?(author)).to eq true
+          expect(event.visible_to_user?(assignee)).to eq true
+          expect(event.visible_to_user?(member)).to eq true
+          expect(event.visible_to_user?(guest)).to eq true
+          expect(event.visible_to_user?(admin)).to eq true
+        end
       end
 
       context 'for confidential issues' do
         let(:target) { confidential_issue }
 
-        it { expect(event.visible_to_user?(non_member)).to eq false }
-        it { expect(event.visible_to_user?(author)).to eq true }
-        it { expect(event.visible_to_user?(assignee)).to eq true }
-        it { expect(event.visible_to_user?(member)).to eq true }
-        it { expect(event.visible_to_user?(guest)).to eq false }
-        it { expect(event.visible_to_user?(admin)).to eq true }
+        it do
+          expect(event.visible_to_user?(non_member)).to eq false
+          expect(event.visible_to_user?(author)).to eq true
+          expect(event.visible_to_user?(assignee)).to eq true
+          expect(event.visible_to_user?(member)).to eq true
+          expect(event.visible_to_user?(guest)).to eq false
+          expect(event.visible_to_user?(admin)).to eq true
+        end
       end
     end
 
@@ -109,23 +135,27 @@ describe Event, models: true do
       context 'on non confidential issues' do
         let(:target) { note_on_issue }
 
-        it { expect(event.visible_to_user?(non_member)).to eq true }
-        it { expect(event.visible_to_user?(author)).to eq true }
-        it { expect(event.visible_to_user?(assignee)).to eq true }
-        it { expect(event.visible_to_user?(member)).to eq true }
-        it { expect(event.visible_to_user?(guest)).to eq true }
-        it { expect(event.visible_to_user?(admin)).to eq true }
+        it do
+          expect(event.visible_to_user?(non_member)).to eq true
+          expect(event.visible_to_user?(author)).to eq true
+          expect(event.visible_to_user?(assignee)).to eq true
+          expect(event.visible_to_user?(member)).to eq true
+          expect(event.visible_to_user?(guest)).to eq true
+          expect(event.visible_to_user?(admin)).to eq true
+        end
       end
 
       context 'on confidential issues' do
         let(:target) { note_on_confidential_issue }
 
-        it { expect(event.visible_to_user?(non_member)).to eq false }
-        it { expect(event.visible_to_user?(author)).to eq true }
-        it { expect(event.visible_to_user?(assignee)).to eq true }
-        it { expect(event.visible_to_user?(member)).to eq true }
-        it { expect(event.visible_to_user?(guest)).to eq false }
-        it { expect(event.visible_to_user?(admin)).to eq true }
+        it do
+          expect(event.visible_to_user?(non_member)).to eq false
+          expect(event.visible_to_user?(author)).to eq true
+          expect(event.visible_to_user?(assignee)).to eq true
+          expect(event.visible_to_user?(member)).to eq true
+          expect(event.visible_to_user?(guest)).to eq false
+          expect(event.visible_to_user?(admin)).to eq true
+        end
       end
     end
 
@@ -135,12 +165,27 @@ describe Event, models: true do
       let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
       let(:target) { note_on_merge_request }
 
-      it { expect(event.visible_to_user?(non_member)).to eq true }
-      it { expect(event.visible_to_user?(author)).to eq true }
-      it { expect(event.visible_to_user?(assignee)).to eq true }
-      it { expect(event.visible_to_user?(member)).to eq true }
-      it { expect(event.visible_to_user?(guest)).to eq true }
-      it { expect(event.visible_to_user?(admin)).to eq true }
+      it do
+        expect(event.visible_to_user?(non_member)).to eq true
+        expect(event.visible_to_user?(author)).to eq true
+        expect(event.visible_to_user?(assignee)).to eq true
+        expect(event.visible_to_user?(member)).to eq true
+        expect(event.visible_to_user?(guest)).to eq true
+        expect(event.visible_to_user?(admin)).to eq true
+      end
+
+      context 'private project' do
+        let(:project) { create(:project, :private) }
+
+        it do
+          expect(event.visible_to_user?(non_member)).to eq false
+          expect(event.visible_to_user?(author)).to eq true
+          expect(event.visible_to_user?(assignee)).to eq true
+          expect(event.visible_to_user?(member)).to eq true
+          expect(event.visible_to_user?(guest)).to eq false
+          expect(event.visible_to_user?(admin)).to eq true
+        end
+      end
     end
   end
 
@@ -161,6 +206,33 @@ describe Event, models: true do
     end
   end
 
+  describe '#reset_project_activity' do
+    let(:project) { create(:empty_project) }
+
+    context 'when a project was updated less than 1 hour ago' do
+      it 'does not update the project' do
+        project.update(last_activity_at: Time.now)
+
+        expect(project).not_to receive(:update_column).
+          with(:last_activity_at, a_kind_of(Time))
+
+        create_event(project, project.owner)
+      end
+    end
+
+    context 'when a project was updated more than 1 hour ago' do
+      it 'updates the project' do
+        project.update(last_activity_at: 1.year.ago)
+
+        create_event(project, project.owner)
+
+        project.reload
+
+        project.last_activity_at <= 1.minute.ago
+      end
+    end
+  end
+
   def create_event(project, user, attrs = {})
     data = {
       before: Gitlab::Git::BLANK_SHA,
@@ -182,6 +254,6 @@ describe Event, models: true do
       action: Event::PUSHED,
       data: data,
       author_id: user.id
-    }.merge(attrs))
+    }.merge!(attrs))
   end
 end
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 4fc3b065592cbfa7997e6ef15022e469a6bdfe94..2debe1289a3f748fb5c2ad70b4f6f99a9f0a0094 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe ExternalIssue, models: true do
-  let(:project) { double('project', to_reference: 'namespace1/project1') }
+  let(:project) { double('project', id: 1, to_reference: 'namespace1/project1') }
   let(:issue)   { described_class.new('EXT-1234', project) }
 
   describe 'modules' do
@@ -10,21 +10,6 @@ describe ExternalIssue, models: true do
     it { is_expected.to include_module(Referable) }
   end
 
-  describe '.reference_pattern' do
-    it 'allows underscores in the project name' do
-      expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
-    end
-
-    it 'allows numbers in the project name' do
-      expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
-    end
-
-    it 'requires the project name to begin with A-Z' do
-      expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil
-      expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
-    end
-  end
-
   describe '#to_reference' do
     it 'returns a String reference to the object' do
       expect(issue.to_reference).to eq issue.id
@@ -51,4 +36,10 @@ describe ExternalIssue, models: true do
       end
     end
   end
+
+  describe '#project_id' do
+    it 'returns the ID of the project' do
+      expect(issue.project_id).to eq(project.id)
+    end
+  end
 end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 9c81d159cdf6da4684bad60b17ab1016dc5e73e9..1863581f57be4a46ae361c1c146cd553e3fe0b88 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
   let(:user) { create(:user, namespace: namespace) }
 
   before do
+    create(:project_member, :reporter, user: user, project: project_from)
     @project_to = fork_project(project_from, user)
   end
 
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 92e0f7f27cecc1939d45b375499b1bb6e5277d53..dd03348052796f33d31e517349713f2dcc7abacf 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do
           milestone1_project2,
           milestone1_project3,
         ]
+      milestones_relation = Milestone.where(id: milestones.map(&:id))
 
-      @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
+      @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones_relation)
     end
 
     it 'has exactly one group milestone' do
@@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do
     let(:milestone) { create(:milestone, title: "git / test", project: project1) }
 
     it 'strips out slashes and spaces' do
-      global_milestone = GlobalMilestone.new(milestone.title, [milestone])
+      global_milestone = GlobalMilestone.new(milestone.title, Milestone.where(id: milestone.id))
 
       expect(global_milestone.safe_title).to eq('git-test')
     end
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2369658bf78e8a55a5e78715aab461c27377fd4c
--- /dev/null
+++ b/spec/models/group_label_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe GroupLabel, models: true do
+  describe 'relationships' do
+    it { is_expected.to belong_to(:group) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:group) }
+  end
+
+  describe '#subject' do
+    it 'aliases group to subject' do
+      subject = described_class.new(group: build(:group))
+
+      expect(subject.subject).to be(subject.group)
+    end
+  end
+
+  describe '#to_reference' do
+    let(:label) { create(:group_label, title: 'feature') }
+
+    context 'using id' do
+      it 'returns a String reference to the object' do
+        expect(label.to_reference).to eq "~#{label.id}"
+      end
+    end
+
+    context 'using name' do
+      it 'returns a String reference to the object' do
+        expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+      end
+
+      it 'uses id when name contains double quote' do
+        label = create(:label, name: %q{"irony"})
+        expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+      end
+    end
+
+    context 'using invalid format' do
+      it 'raises error' do
+        expect { label.to_reference(format: :invalid) }
+          .to raise_error StandardError, /Unknown format/
+      end
+    end
+  end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ea4b59c26b1e6d8274a3669b3f868e59d13e8533..47f89f744cb74c1138ed82bf6487de865ef993bf 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -12,6 +12,7 @@ describe Group, models: true do
     it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
     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') }
 
     describe '#members & #requesters' do
       let(:requester) { create(:user) }
@@ -187,6 +188,52 @@ describe Group, models: true do
     it { expect(group.has_master?(@members[:requester])).to be_falsey }
   end
 
+  describe '#lfs_enabled?' do
+    context 'LFS enabled globally' do
+      before do
+        allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+      end
+
+      it 'returns true when nothing is set' do
+        expect(group.lfs_enabled?).to be_truthy
+      end
+
+      it 'returns false when set to false' do
+        group.update_attribute(:lfs_enabled, false)
+
+        expect(group.lfs_enabled?).to be_falsey
+      end
+
+      it 'returns true when set to true' do
+        group.update_attribute(:lfs_enabled, true)
+
+        expect(group.lfs_enabled?).to be_truthy
+      end
+    end
+
+    context 'LFS disabled globally' do
+      before do
+        allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+      end
+
+      it 'returns false when nothing is set' do
+        expect(group.lfs_enabled?).to be_falsey
+      end
+
+      it 'returns false when set to false' do
+        group.update_attribute(:lfs_enabled, false)
+
+        expect(group.lfs_enabled?).to be_falsey
+      end
+
+      it 'returns false when set to true' do
+        group.update_attribute(:lfs_enabled, true)
+
+        expect(group.lfs_enabled?).to be_falsey
+      end
+    end
+  end
+
   describe '#owners' do
     let(:owner) { create(:user) }
     let(:developer) { create(:user) }
@@ -218,4 +265,10 @@ describe Group, models: true do
 
     members
   end
+
+  describe '#web_url' do
+    it 'returns the canonical URL' do
+      expect(group.web_url).to include("groups/#{group.name}")
+    end
+  end
 end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d79f929f7a1ca0aca42043cce986495e473b097e
--- /dev/null
+++ b/spec/models/guest_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Guest, lib: true do
+  let(:public_project) { create(:project, :public) }
+  let(:private_project) { create(:project, :private) }
+  let(:internal_project) { create(:project, :internal) }
+
+  describe '.can_pull?' do
+    context 'when project is private' do
+      it 'does not allow to pull the repo' do
+        expect(Guest.can?(:download_code, private_project)).to eq(false)
+      end
+    end
+
+    context 'when project is internal' do
+      it 'does not allow to pull the repo' do
+        expect(Guest.can?(:download_code, internal_project)).to eq(false)
+      end
+    end
+
+    context 'when project is public' do
+      context 'when repository is disabled' do
+        it 'does not allow to pull the repo' do
+          public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+          expect(Guest.can?(:download_code, public_project)).to eq(false)
+        end
+      end
+
+      context 'when repository is accessible only by team members' do
+        it 'does not allow to pull the repo' do
+          public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE)
+
+          expect(Guest.can?(:download_code, public_project)).to eq(false)
+        end
+      end
+
+      context 'when repository is enabled' do
+        it 'allows to pull the repo' do
+          public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+          expect(Guest.can?(:download_code, public_project)).to eq(true)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 4a457997a4fa9dd8997bc8ed3049e1a0ffb2ed21..474ae62cceca6d40fb70fbcdcd83d4cc5713a162 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-#  id                    :integer          not null, primary key
-#  url                   :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  type                  :string(255)      default("ProjectHook")
-#  service_id            :integer
-#  push_events           :boolean          default(TRUE), not null
-#  issues_events         :boolean          default(FALSE), not null
-#  merge_requests_events :boolean          default(FALSE), not null
-#  tag_push_events       :boolean          default(FALSE)
-#  note_events           :boolean          default(FALSE), not null
-#
-
 require 'spec_helper'
 
 describe ProjectHook, models: true do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 534e1b4f1285e12f156ec823a41799c91cff0de8..1a83c836652ebd2c15667f64f19511872f4b8e15 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-#  id                    :integer          not null, primary key
-#  url                   :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  type                  :string(255)      default("ProjectHook")
-#  service_id            :integer
-#  push_events           :boolean          default(TRUE), not null
-#  issues_events         :boolean          default(FALSE), not null
-#  merge_requests_events :boolean          default(FALSE), not null
-#  tag_push_events       :boolean          default(FALSE)
-#  note_events           :boolean          default(FALSE), not null
-#
-
 require "spec_helper"
 
 describe ServiceHook, models: true do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index cbdf7eec082d4832c99691babb7cd96396f377eb..ad2b710041a38b3f3974e5a73f0031fc1c94b56f 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-#  id                    :integer          not null, primary key
-#  url                   :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  type                  :string(255)      default("ProjectHook")
-#  service_id            :integer
-#  push_events           :boolean          default(TRUE), not null
-#  issues_events         :boolean          default(FALSE), not null
-#  merge_requests_events :boolean          default(FALSE), not null
-#  tag_push_events       :boolean          default(FALSE)
-#  note_events           :boolean          default(FALSE), not null
-#
-
 require "spec_helper"
 
 describe SystemHook, models: true do
@@ -48,7 +30,7 @@ describe SystemHook, models: true do
 
     it "user_create hook" do
       create(:user)
-      
+
       expect(WebMock).to have_requested(:post, system_hook.url).with(
         body: /user_create/,
         headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index f9bab487b96338bb1c68f80eeca03f754b5d8f30..e52b9d75cefab6713e5ff8e04448f4fbeab00751 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-#  id                    :integer          not null, primary key
-#  url                   :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  type                  :string(255)      default("ProjectHook")
-#  service_id            :integer
-#  push_events           :boolean          default(TRUE), not null
-#  issues_events         :boolean          default(FALSE), not null
-#  merge_requests_events :boolean          default(FALSE), not null
-#  tag_push_events       :boolean          default(FALSE)
-#  note_events           :boolean          default(FALSE), not null
-#
-
 require 'spec_helper'
 
 describe WebHook, models: true do
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2459a49f0958b4daebf20f0c7a4f31feaeecd174
--- /dev/null
+++ b/spec/models/issue/metrics_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Issue::Metrics, models: true do
+  let(:project) { create(:project) }
+
+  subject { create(:issue, project: project) }
+
+  describe "when recording the default set of issue metrics on issue save" do
+    context "milestones" do
+      it "records the first time an issue is associated with a milestone" do
+        time = Time.now
+        Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+        metrics = subject.metrics
+
+        expect(metrics).to be_present
+        expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
+      end
+
+      it "does not record the second time an issue is associated with a milestone" do
+        time = Time.now
+        Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+        Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) }
+        Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) }
+        metrics = subject.metrics
+
+        expect(metrics).to be_present
+        expect(metrics.first_associated_with_milestone_at).to be_like_time(time)
+      end
+    end
+
+    context "list labels" do
+      it "records the first time an issue is associated with a list label" do
+        list_label = create(:label, lists: [create(:list)])
+        time = Time.now
+        Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) }
+        metrics = subject.metrics
+
+        expect(metrics).to be_present
+        expect(metrics.first_added_to_board_at).to be_like_time(time)
+      end
+
+      it "does not record the second time an issue is associated with a list label" do
+        time = Time.now
+        first_list_label = create(:label, lists: [create(:list)])
+        Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) }
+        second_list_label = create(:label, lists: [create(:list)])
+        Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) }
+        metrics = subject.metrics
+
+        expect(metrics).to be_present
+        expect(metrics.first_added_to_board_at).to be_like_time(time)
+      end
+    end
+  end
+end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d742c8146802c2848ca9495662310fa714daf243
--- /dev/null
+++ b/spec/models/issue_collection_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe IssueCollection do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:issue1) { create(:issue, project: project) }
+  let(:issue2) { create(:issue, project: project) }
+  let(:collection) { described_class.new([issue1, issue2]) }
+
+  describe '#collection' do
+    it 'returns the issues in the same order as the input Array' do
+      expect(collection.collection).to eq([issue1, issue2])
+    end
+  end
+
+  describe '#updatable_by_user' do
+    context 'using an admin user' do
+      it 'returns all issues' do
+        user = create(:admin)
+
+        expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
+      end
+    end
+
+    context 'using a user that has no access to the project' do
+      it 'returns no issues when the user is not an assignee or author' do
+        expect(collection.updatable_by_user(user)).to be_empty
+      end
+
+      it 'returns the issues the user is assigned to' do
+        issue1.assignee = user
+
+        expect(collection.updatable_by_user(user)).to eq([issue1])
+      end
+
+      it 'returns the issues for which the user is the author' do
+        issue1.author = user
+
+        expect(collection.updatable_by_user(user)).to eq([issue1])
+      end
+    end
+
+    context 'using a user that has reporter access to the project' do
+      it 'returns the issues of the project' do
+        project.team << [user, :reporter]
+
+        expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
+      end
+    end
+
+    context 'using a user that is the owner of a project' do
+      it 'returns the issues of the project' do
+        expect(collection.updatable_by_user(project.namespace.owner)).
+          to eq([issue1, issue2])
+      end
+    end
+  end
+
+  describe '#visible_to' do
+    it 'is an alias for updatable_by_user' do
+      updatable_by_user = described_class.instance_method(:updatable_by_user)
+      visible_to = described_class.instance_method(:visible_to)
+
+      expect(visible_to).to eq(updatable_by_user)
+    end
+  end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 3259f79529647c10c17959bb9de5a3a2ab37ad10..300425767ed9eef901fc324d47a3f47ecfffe366 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,7 +22,7 @@ describe Issue, models: true do
     it { is_expected.to have_db_index(:deleted_at) }
   end
 
-  describe 'visible_to_user' do
+  describe '.visible_to_user' do
     let(:user) { create(:user) }
     let(:authorized_user) { create(:user) }
     let(:project) { create(:project, namespace: authorized_user.namespace) }
@@ -100,13 +100,19 @@ describe Issue, models: true do
     end
 
     it 'returns the merge request to close this issue' do
-      allow(mr).to receive(:closes_issue?).with(issue).and_return(true)
+      mr
 
-      expect(issue.closed_by_merge_requests).to eq([mr])
+      expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
+    end
+
+    it "returns an empty array when the merge request is closed already" do
+      closed_mr
+
+      expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
     end
 
     it "returns an empty array when the current issue is closed already" do
-      expect(closed_issue.closed_by_merge_requests).to eq([])
+      expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([])
     end
   end
 
@@ -212,7 +218,7 @@ describe Issue, models: true do
                                                source_project: subject.project,
                                                source_branch: "#{subject.iid}-branch" })
       merge_request.create_cross_references!(user)
-      expect(subject.referenced_merge_requests).not_to be_empty
+      expect(subject.referenced_merge_requests(user)).not_to be_empty
       expect(subject.related_branches(user)).to eq([subject.to_branch_name])
     end
 
@@ -308,6 +314,22 @@ describe Issue, models: true do
   end
 
   describe '#visible_to_user?' do
+    context 'without a user' do
+      let(:issue) { build(:issue) }
+
+      it 'returns true when the issue is publicly visible' do
+        expect(issue).to receive(:publicly_visible?).and_return(true)
+
+        expect(issue.visible_to_user?).to eq(true)
+      end
+
+      it 'returns false when the issue is not publicly visible' do
+        expect(issue).to receive(:publicly_visible?).and_return(false)
+
+        expect(issue.visible_to_user?).to eq(false)
+      end
+    end
+
     context 'with a user' do
       let(:user) { build(:user) }
       let(:issue) { build(:issue) }
@@ -323,26 +345,24 @@ describe Issue, models: true do
 
         expect(issue.visible_to_user?(user)).to eq(false)
       end
-    end
 
-    context 'without a user' do
-      let(:issue) { build(:issue) }
+      it 'returns false when feature is disabled' do
+        expect(issue).not_to receive(:readable_by?)
 
-      it 'returns true when the issue is publicly visible' do
-        expect(issue).to receive(:publicly_visible?).and_return(true)
+        issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
 
-        expect(issue.visible_to_user?).to eq(true)
+        expect(issue.visible_to_user?(user)).to eq(false)
       end
 
-      it 'returns false when the issue is not publicly visible' do
-        expect(issue).to receive(:publicly_visible?).and_return(false)
+      it 'returns false when restricted for members' do
+        expect(issue).not_to receive(:readable_by?)
 
-        expect(issue.visible_to_user?).to eq(false)
+        issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
+
+        expect(issue.visible_to_user?(user)).to eq(false)
       end
     end
-  end
 
-  describe '#readable_by?' do
     describe 'with a regular user that is not a team member' do
       let(:user) { create(:user) }
 
@@ -352,13 +372,13 @@ describe Issue, models: true do
         it 'returns true for a regular issue' do
           issue = build(:issue, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
 
         it 'returns false for a confidential issue' do
           issue = build(:issue, project: project, confidential: true)
 
-          expect(issue).not_to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(false)
         end
       end
 
@@ -369,13 +389,13 @@ describe Issue, models: true do
           it 'returns true for a regular issue' do
             issue = build(:issue, project: project)
 
-            expect(issue).to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(true)
           end
 
           it 'returns false for a confidential issue' do
             issue = build(:issue, :confidential, project: project)
 
-            expect(issue).not_to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(false)
           end
         end
 
@@ -387,13 +407,13 @@ describe Issue, models: true do
           it 'returns false for a regular issue' do
             issue = build(:issue, project: project)
 
-            expect(issue).not_to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(false)
           end
 
           it 'returns false for a confidential issue' do
             issue = build(:issue, :confidential, project: project)
 
-            expect(issue).not_to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(false)
           end
         end
       end
@@ -404,26 +424,28 @@ describe Issue, models: true do
         it 'returns false for a regular issue' do
           issue = build(:issue, project: project)
 
-          expect(issue).not_to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(false)
         end
 
         it 'returns false for a confidential issue' do
           issue = build(:issue, :confidential, project: project)
 
-          expect(issue).not_to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(false)
         end
 
         context 'when the user is the project owner' do
+          before { project.team << [user, :master] }
+
           it 'returns true for a regular issue' do
             issue = build(:issue, project: project)
 
-            expect(issue).not_to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(true)
           end
 
           it 'returns true for a confidential issue' do
             issue = build(:issue, :confidential, project: project)
 
-            expect(issue).not_to be_readable_by(user)
+            expect(issue.visible_to_user?(user)).to eq(true)
           end
         end
       end
@@ -441,13 +463,13 @@ describe Issue, models: true do
         it 'returns true for a regular issue' do
           issue = build(:issue, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
 
         it 'returns true for a confidential issue' do
           issue = build(:issue, :confidential, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
       end
 
@@ -461,13 +483,13 @@ describe Issue, models: true do
         it 'returns true for a regular issue' do
           issue = build(:issue, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
 
         it 'returns true for a confidential issue' do
           issue = build(:issue, :confidential, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
       end
 
@@ -481,31 +503,31 @@ describe Issue, models: true do
         it 'returns true for a regular issue' do
           issue = build(:issue, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
 
         it 'returns true for a confidential issue' do
           issue = build(:issue, :confidential, project: project)
 
-          expect(issue).to be_readable_by(user)
+          expect(issue.visible_to_user?(user)).to eq(true)
         end
       end
     end
 
     context 'with an admin user' do
       let(:project) { create(:empty_project) }
-      let(:user) { create(:user, admin: true) }
+      let(:user) { create(:admin) }
 
       it 'returns true for a regular issue' do
         issue = build(:issue, project: project)
 
-        expect(issue).to be_readable_by(user)
+        expect(issue.visible_to_user?(user)).to eq(true)
       end
 
       it 'returns true for a confidential issue' do
         issue = build(:issue, :confidential, project: project)
 
-        expect(issue).to be_readable_by(user)
+        expect(issue.visible_to_user?(user)).to eq(true)
       end
     end
   end
@@ -517,13 +539,13 @@ describe Issue, models: true do
       it 'returns true for a regular issue' do
         issue = build(:issue, project: project)
 
-        expect(issue).to be_publicly_visible
+        expect(issue).to be_truthy
       end
 
       it 'returns false for a confidential issue' do
         issue = build(:issue, :confidential, project: project)
 
-        expect(issue).not_to be_publicly_visible
+        expect(issue).not_to be_falsy
       end
     end
 
@@ -533,13 +555,13 @@ describe Issue, models: true do
       it 'returns false for a regular issue' do
         issue = build(:issue, project: project)
 
-        expect(issue).not_to be_publicly_visible
+        expect(issue).not_to be_falsy
       end
 
       it 'returns false for a confidential issue' do
         issue = build(:issue, :confidential, project: project)
 
-        expect(issue).not_to be_publicly_visible
+        expect(issue).not_to be_falsy
       end
     end
 
@@ -549,13 +571,13 @@ describe Issue, models: true do
       it 'returns false for a regular issue' do
         issue = build(:issue, project: project)
 
-        expect(issue).not_to be_publicly_visible
+        expect(issue).not_to be_falsy
       end
 
       it 'returns false for a confidential issue' do
         issue = build(:issue, :confidential, project: project)
 
-        expect(issue).not_to be_publicly_visible
+        expect(issue).not_to be_falsy
       end
     end
   end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index fd4a2beff586f191d4f8f6ab2155efd8e502e239..7fc6ed1dd546d4176f90e97c415483f39bfe76d8 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -5,9 +5,6 @@ describe Key, models: true do
     it { is_expected.to belong_to(:user) }
   end
 
-  describe "Mass assignment" do
-  end
-
   describe "Validation" do
     it { is_expected.to validate_presence_of(:title) }
     it { is_expected.to validate_presence_of(:key) }
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index 5e6f8ca1528151ade39081c01ffcf8bc58e3a755..c18ed8574b1e004675ec5d2dd6ae34df3e299ed1 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -1,8 +1,7 @@
 require 'spec_helper'
 
 describe LabelLink, models: true do
-  let(:label) { create(:label_link) }
-  it { expect(label).to be_valid }
+  it { expect(build(:label_link)).to be_valid }
 
   it { is_expected.to belong_to(:label) }
   it { is_expected.to belong_to(:target) }
diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d18c2f7949a85b6e8e3b2294548b083e33f95d4d
--- /dev/null
+++ b/spec/models/label_priority_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe LabelPriority, models: true do
+  describe 'relationships' do
+    it { is_expected.to belong_to(:project) }
+    it { is_expected.to belong_to(:label) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:project) }
+    it { is_expected.to validate_presence_of(:label) }
+    it { is_expected.to validate_numericality_of(:priority).only_integer.is_greater_than_or_equal_to(0) }
+
+    it 'validates uniqueness of label_id scoped to project_id' do
+      create(:label_priority)
+
+      expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id)
+    end
+  end
+end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 2a09063f85779ed0fe92a69faffda2a5dd2f066d..0c163659a717ab11279244194eea4a5bb2a4f003 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -1,44 +1,42 @@
 require 'spec_helper'
 
 describe Label, models: true do
-  let(:label) { create(:label) }
+  describe 'modules' do
+    it { is_expected.to include_module(Referable) }
+    it { is_expected.to include_module(Subscribable) }
+  end
 
   describe 'associations' do
-    it { is_expected.to belong_to(:project) }
-    it { is_expected.to have_many(:label_links).dependent(:destroy) }
     it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
-  end
-
-  describe 'modules' do
-    subject { described_class }
-
-    it { is_expected.to include_module(Referable) }
+    it { is_expected.to have_many(:label_links).dependent(:destroy) }
+    it { is_expected.to have_many(:lists).dependent(:destroy) }
+    it { is_expected.to have_many(:priorities).class_name('LabelPriority') }
   end
 
   describe 'validation' do
-    it { is_expected.to validate_presence_of(:project) }
+    it { is_expected.to validate_uniqueness_of(:title).scoped_to([:group_id, :project_id]) }
 
     it 'validates color code' do
-      expect(label).not_to allow_value('G-ITLAB').for(:color)
-      expect(label).not_to allow_value('AABBCC').for(:color)
-      expect(label).not_to allow_value('#AABBCCEE').for(:color)
-      expect(label).not_to allow_value('GGHHII').for(:color)
-      expect(label).not_to allow_value('#').for(:color)
-      expect(label).not_to allow_value('').for(:color)
-
-      expect(label).to allow_value('#AABBCC').for(:color)
-      expect(label).to allow_value('#abcdef').for(:color)
+      is_expected.not_to allow_value('G-ITLAB').for(:color)
+      is_expected.not_to allow_value('AABBCC').for(:color)
+      is_expected.not_to allow_value('#AABBCCEE').for(:color)
+      is_expected.not_to allow_value('GGHHII').for(:color)
+      is_expected.not_to allow_value('#').for(:color)
+      is_expected.not_to allow_value('').for(:color)
+
+      is_expected.to allow_value('#AABBCC').for(:color)
+      is_expected.to allow_value('#abcdef').for(:color)
     end
 
     it 'validates title' do
-      expect(label).not_to allow_value('G,ITLAB').for(:title)
-      expect(label).not_to allow_value('').for(:title)
-
-      expect(label).to allow_value('GITLAB').for(:title)
-      expect(label).to allow_value('gitlab').for(:title)
-      expect(label).to allow_value('G?ITLAB').for(:title)
-      expect(label).to allow_value('G&ITLAB').for(:title)
-      expect(label).to allow_value("customer's request").for(:title)
+      is_expected.not_to allow_value('G,ITLAB').for(:title)
+      is_expected.not_to allow_value('').for(:title)
+
+      is_expected.to allow_value('GITLAB').for(:title)
+      is_expected.to allow_value('gitlab').for(:title)
+      is_expected.to allow_value('G?ITLAB').for(:title)
+      is_expected.to allow_value('G&ITLAB').for(:title)
+      is_expected.to allow_value("customer's request").for(:title)
     end
   end
 
@@ -49,45 +47,59 @@ describe Label, models: true do
     end
   end
 
-  describe '#to_reference' do
-    context 'using id' do
-      it 'returns a String reference to the object' do
-        expect(label.to_reference).to eq "~#{label.id}"
-      end
-    end
+  describe 'priorization' do
+    subject(:label) { create(:label) }
+
+    let(:project) { label.project }
 
-    context 'using name' do
-      it 'returns a String reference to the object' do
-        expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+    describe '#prioritize!' do
+      context 'when label is not prioritized' do
+        it 'creates a label priority' do
+          expect { label.prioritize!(project, 1) }.to change(label.priorities, :count).by(1)
+        end
+
+        it 'sets label priority' do
+          label.prioritize!(project, 1)
+
+          expect(label.priorities.first.priority).to eq 1
+        end
       end
 
-      it 'uses id when name contains double quote' do
-        label = create(:label, name: %q{"irony"})
-        expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+      context 'when label is prioritized' do
+        let!(:priority) { create(:label_priority, project: project, label: label, priority: 0) }
+
+        it 'does not create a label priority' do
+          expect { label.prioritize!(project, 1) }.not_to change(label.priorities, :count)
+        end
+
+        it 'updates label priority' do
+          label.prioritize!(project, 1)
+
+          expect(priority.reload.priority).to eq 1
+        end
       end
     end
 
-    context 'using invalid format' do
-      it 'raises error' do
-        expect { label.to_reference(format: :invalid) }
-          .to raise_error StandardError, /Unknown format/
+    describe '#unprioritize!' do
+      it 'removes label priority' do
+        create(:label_priority, project: project, label: label, priority: 0)
+
+        expect { label.unprioritize!(project) }.to change(label.priorities, :count).by(-1)
       end
     end
 
-    context 'cross project reference' do
-      let(:project) { create(:project) }
-
-      context 'using name' do
-        it 'returns cross reference with label name' do
-          expect(label.to_reference(project, format: :name))
-            .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+    describe '#priority' do
+      context 'when label is not prioritized' do
+        it 'returns nil' do
+          expect(label.priority(project)).to be_nil
         end
       end
 
-      context 'using id' do
-        it 'returns cross reference with label id' do
-          expect(label.to_reference(project, format: :id))
-            .to eq %Q(#{label.project.to_reference}~#{label.id})
+      context 'when label is prioritized' do
+        it 'returns label priority' do
+          create(:label_priority, project: project, label: label, priority: 1)
+
+          expect(label.priority(project)).to eq 1
         end
       end
     end
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index 2cfd26419ca0869f18a6b16d316523e3e86fb0b1..81517a18b748604cfaefcaca70ac6c5b9a226cab 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -73,4 +73,29 @@ describe LegacyDiffNote, models: true do
       end
     end
   end
+
+  describe "#discussion_id" do
+    let(:note) { create(:note) }
+
+    context "when it is newly created" do
+      it "has a discussion id" do
+        expect(note.discussion_id).not_to be_nil
+        expect(note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+
+    context "when it didn't store a discussion id before" do
+      before do
+        note.update_column(:discussion_id, nil)
+      end
+
+      it "has a discussion id" do
+        # The discussion_id is set in `after_initialize`, so `reload` won't work
+        reloaded_note = Note.find(note.id)
+
+        expect(reloaded_note.discussion_id).not_to be_nil
+        expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+  end
 end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e1a52011c3789702a0061327dfb519be864b748
--- /dev/null
+++ b/spec/models/list_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+describe List do
+  describe 'relationships' do
+    it { is_expected.to belong_to(:board) }
+    it { is_expected.to belong_to(:label) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:board) }
+    it { is_expected.to validate_presence_of(:label) }
+    it { is_expected.to validate_presence_of(:list_type) }
+    it { is_expected.to validate_presence_of(:position) }
+    it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) }
+
+    it 'validates uniqueness of label scoped to board_id' do
+      create(:list)
+
+      expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
+    end
+
+    context 'when list_type is set to backlog' do
+      subject { described_class.new(list_type: :backlog) }
+
+      it { is_expected.not_to validate_presence_of(:label) }
+      it { is_expected.not_to validate_presence_of(:position) }
+    end
+
+    context 'when list_type is set to done' do
+      subject { described_class.new(list_type: :done) }
+
+      it { is_expected.not_to validate_presence_of(:label) }
+      it { is_expected.not_to validate_presence_of(:position) }
+    end
+  end
+
+  describe '#destroy' do
+    it 'can be destroyed when when list_type is set to label' do
+      subject = create(:list)
+
+      expect(subject.destroy).to be_truthy
+    end
+
+    it 'can not be destroyed when list_type is set to backlog' do
+      subject = create(:backlog_list)
+
+      expect(subject.destroy).to be_falsey
+    end
+
+    it 'can not be destroyed when when list_type is set to done' do
+      subject = create(:done_list)
+
+      expect(subject.destroy).to be_falsey
+    end
+  end
+
+  describe '#destroyable?' do
+    it 'retruns true when list_type is set to label' do
+      subject.list_type = :label
+
+      expect(subject).to be_destroyable
+    end
+
+    it 'retruns false when list_type is set to backlog' do
+      subject.list_type = :backlog
+
+      expect(subject).not_to be_destroyable
+    end
+
+    it 'retruns false when list_type is set to done' do
+      subject.list_type = :done
+
+      expect(subject).not_to be_destroyable
+    end
+  end
+
+  describe '#movable?' do
+    it 'retruns true when list_type is set to label' do
+      subject.list_type = :label
+
+      expect(subject).to be_movable
+    end
+
+    it 'retruns false when list_type is set to backlog' do
+      subject.list_type = :backlog
+
+      expect(subject).not_to be_movable
+    end
+
+    it 'retruns false when list_type is set to done' do
+      subject.list_type = :done
+
+      expect(subject).not_to be_movable
+    end
+  end
+
+  describe '#title' do
+    it 'returns label name when list_type is set to label' do
+      subject.list_type = :label
+      subject.label = Label.new(name: 'Development')
+
+      expect(subject.title).to eq 'Development'
+    end
+
+    it 'returns Backlog when list_type is set to backlog' do
+      subject.list_type = :backlog
+
+      expect(subject.title).to eq 'Backlog'
+    end
+
+    it 'returns Done when list_type is set to done' do
+      subject.list_type = :done
+
+      expect(subject.title).to eq 'Done'
+    end
+  end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2277f4e13bfd84877275758fd325e3bebce6869d..485121701af1f4b802c6ac8c349a035020ac4a1e 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
 
   describe 'Scopes & finders' do
     before do
-      project = create(:project)
+      project = create(:empty_project, :public)
       group = create(:group)
       @owner_user = create(:user).tap { |u| group.add_owner(u) }
       @owner = group.members.find_by(user_id: @owner_user.id)
@@ -65,12 +65,26 @@ describe Member, models: true do
       @master_user = create(:user).tap { |u| project.team << [u, :master] }
       @master = project.members.find_by(user_id: @master_user.id)
 
-      ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
-      @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+      @blocked_user = create(:user).tap do |u|
+        project.team << [u, :master]
+        project.team << [u, :developer]
 
-      accepted_invite_user = build(:user)
-      ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
-      @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+        u.block!
+      end
+      @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
+      @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
+
+      @invited_member = create(:project_member, :developer,
+                              project: project,
+                              invite_token: '1234',
+                              invite_email: 'toto1@example.com')
+
+      accepted_invite_user = build(:user, state: :active)
+      @accepted_invite_member = create(:project_member, :developer,
+                                      project: project,
+                                      invite_token: '1234',
+                                      invite_email: 'toto2@example.com').
+                                      tap { |u| u.accept_invite!(accepted_invite_user) }
 
       requested_user = create(:user).tap { |u| project.request_access(u) }
       @requested_member = project.requesters.find_by(user_id: requested_user.id)
@@ -81,7 +95,7 @@ describe Member, models: true do
 
     describe '.access_for_user_ids' do
       it 'returns the right access levels' do
-        users = [@owner_user.id, @master_user.id]
+        users = [@owner_user.id, @master_user.id, @blocked_user.id]
         expected = {
           @owner_user.id => Gitlab::Access::OWNER,
           @master_user.id => Gitlab::Access::MASTER
@@ -115,6 +129,19 @@ describe Member, models: true do
       it { expect(described_class.request).not_to include @accepted_request_member }
     end
 
+    describe '.developers' do
+      subject { described_class.developers.to_a }
+
+      it { is_expected.not_to include @owner }
+      it { is_expected.not_to include @master }
+      it { is_expected.to include @invited_member }
+      it { is_expected.to include @accepted_invite_member }
+      it { is_expected.not_to include @requested_member }
+      it { is_expected.to include @accepted_request_member }
+      it { is_expected.not_to include @blocked_master }
+      it { is_expected.not_to include @blocked_developer }
+    end
+
     describe '.owners_and_masters' do
       it { expect(described_class.owners_and_masters).to include @owner }
       it { expect(described_class.owners_and_masters).to include @master }
@@ -122,6 +149,20 @@ describe Member, models: true do
       it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
       it { expect(described_class.owners_and_masters).not_to include @requested_member }
       it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+      it { expect(described_class.owners_and_masters).not_to include @blocked_master }
+    end
+
+    describe '.has_access' do
+      subject { described_class.has_access.to_a }
+
+      it { is_expected.to include @owner }
+      it { is_expected.to include @master }
+      it { is_expected.to include @invited_member }
+      it { is_expected.to include @accepted_invite_member }
+      it { is_expected.not_to include @requested_member }
+      it { is_expected.to include @accepted_request_member }
+      it { is_expected.not_to include @blocked_master }
+      it { is_expected.not_to include @blocked_developer }
     end
   end
 
@@ -130,39 +171,209 @@ describe Member, models: true do
     it { is_expected.to respond_to(:user_email) }
   end
 
-  describe ".add_user" do
-    let!(:user)    { create(:user) }
-    let(:project) { create(:project) }
+  describe '.add_user' do
+    %w[project group].each do |source_type|
+      context "when source is a #{source_type}" do
+        let!(:source) { create(source_type, :public) }
+        let!(:user) { create(:user) }
+        let!(:admin) { create(:admin) }
 
-    context "when called with a user id" do
-      it "adds the user as a member" do
-        Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
+        it 'returns a <Source>Member object' do
+          member = described_class.add_user(source, user, :master)
 
-        expect(project.users).to include(user)
-      end
-    end
+          expect(member).to be_a "#{source_type.classify}Member".constantize
+          expect(member).to be_persisted
+        end
 
-    context "when called with a user object" do
-      it "adds the user as a member" do
-        Member.add_user(project.project_members, user, ProjectMember::MASTER)
+        it 'sets members.created_by to the given current_user' do
+          member = described_class.add_user(source, user, :master, current_user: admin)
 
-        expect(project.users).to include(user)
-      end
-    end
+          expect(member.created_by).to eq(admin)
+        end
 
-    context "when called with a known user email" do
-      it "adds the user as a member" do
-        Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
+        it 'sets members.expires_at to the given expires_at' do
+          member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22))
 
-        expect(project.users).to include(user)
-      end
-    end
+          expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+        end
+
+        described_class.access_levels.each do |sym_key, int_access_level|
+          it "accepts the :#{sym_key} symbol as access level" do
+            expect(source.users).not_to include(user)
+
+            member = described_class.add_user(source, user.id, sym_key)
+
+            expect(member.access_level).to eq(int_access_level)
+            expect(source.users.reload).to include(user)
+          end
+
+          it "accepts the #{int_access_level} integer as access level" do
+            expect(source.users).not_to include(user)
+
+            member = described_class.add_user(source, user.id, int_access_level)
+
+            expect(member.access_level).to eq(int_access_level)
+            expect(source.users.reload).to include(user)
+          end
+        end
+
+        context 'with no current_user' do
+          context 'when called with a known user id' do
+            it 'adds the user as a member' do
+              expect(source.users).not_to include(user)
+
+              described_class.add_user(source, user.id, :master)
+
+              expect(source.users.reload).to include(user)
+            end
+          end
+
+          context 'when called with an unknown user id' do
+            it 'adds the user as a member' do
+              expect(source.users).not_to include(user)
+
+              described_class.add_user(source, 42, :master)
+
+              expect(source.users.reload).not_to include(user)
+            end
+          end
+
+          context 'when called with a user object' do
+            it 'adds the user as a member' do
+              expect(source.users).not_to include(user)
+
+              described_class.add_user(source, user, :master)
+
+              expect(source.users.reload).to include(user)
+            end
+          end
+
+          context 'when called with a requester user object' do
+            before do
+              source.request_access(user)
+            end
+
+            it 'adds the requester as a member' do
+              expect(source.users).not_to include(user)
+              expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+              expect { described_class.add_user(source, user, :master) }.
+                to raise_error(Gitlab::Access::AccessDeniedError)
+
+              expect(source.users.reload).not_to include(user)
+              expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
+            end
+          end
+
+          context 'when called with a known user email' do
+            it 'adds the user as a member' do
+              expect(source.users).not_to include(user)
+
+              described_class.add_user(source, user.email, :master)
+
+              expect(source.users.reload).to include(user)
+            end
+          end
+
+          context 'when called with an unknown user email' do
+            it 'creates an invited member' do
+              expect(source.users).not_to include(user)
+
+              described_class.add_user(source, 'user@example.com', :master)
+
+              expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
+            end
+          end
+        end
+
+        context 'when current_user can update member' do
+          it 'creates the member' do
+            expect(source.users).not_to include(user)
+
+            described_class.add_user(source, user, :master, current_user: admin)
+
+            expect(source.users.reload).to include(user)
+          end
+
+          context 'when called with a requester user object' do
+            before do
+              source.request_access(user)
+            end
+
+            it 'adds the requester as a member' do
+              expect(source.users).not_to include(user)
+              expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+              described_class.add_user(source, user, :master, current_user: admin)
+
+              expect(source.users.reload).to include(user)
+              expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
+            end
+          end
+        end
+
+        context 'when current_user cannot update member' do
+          it 'does not create the member' do
+            expect(source.users).not_to include(user)
+
+            member = described_class.add_user(source, user, :master, current_user: user)
+
+            expect(source.users.reload).not_to include(user)
+            expect(member).not_to be_persisted
+          end
+
+          context 'when called with a requester user object' do
+            before do
+              source.request_access(user)
+            end
+
+            it 'does not destroy the requester' do
+              expect(source.users).not_to include(user)
+              expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+              described_class.add_user(source, user, :master, current_user: user)
+
+              expect(source.users.reload).not_to include(user)
+              expect(source.requesters.exists?(user_id: user)).to be_truthy
+            end
+          end
+        end
+
+        context 'when member already exists' do
+          before do
+            source.add_user(user, :developer)
+          end
+
+          context 'with no current_user' do
+            it 'updates the member' do
+              expect(source.users).to include(user)
+
+              described_class.add_user(source, user, :master)
+
+              expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+            end
+          end
+
+          context 'when current_user can update member' do
+            it 'updates the member' do
+              expect(source.users).to include(user)
+
+              described_class.add_user(source, user, :master, current_user: admin)
+
+              expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+            end
+          end
+
+          context 'when current_user cannot update member' do
+            it 'does not update the member' do
+              expect(source.users).to include(user)
 
-    context "when called with an unknown user email" do
-      it "adds a member invite" do
-        Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
+              described_class.add_user(source, user, :master, current_user: user)
 
-        expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com")
+              expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
+            end
+          end
+        end
       end
     end
   end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 4f875fd257a5fa18e155e082ca8dcacc5686f07a..370aeb9e0a920adf692e3eeb358748bdaf4422e4 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,25 +1,33 @@
-# == Schema Information
-#
-# Table name: members
-#
-#  id                 :integer          not null, primary key
-#  access_level       :integer          not null
-#  source_id          :integer          not null
-#  source_type        :string(255)      not null
-#  user_id            :integer
-#  notification_level :integer          not null
-#  type               :string(255)
-#  created_at         :datetime
-#  updated_at         :datetime
-#  created_by_id      :integer
-#  invite_email       :string(255)
-#  invite_token       :string(255)
-#  invite_accepted_at :datetime
-#
-
 require 'spec_helper'
 
 describe GroupMember, models: true do
+  describe '.access_level_roles' do
+    it 'returns Gitlab::Access.options_with_owner' do
+      expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
+    end
+  end
+
+  describe '.access_levels' do
+    it 'returns Gitlab::Access.options_with_owner' do
+      expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
+    end
+  end
+
+  describe '.add_users_to_group' do
+    it 'adds the given users to the given group' do
+      group = create(:group)
+      users = create_list(:user, 2)
+
+      described_class.add_users_to_group(
+        group,
+        [users.first.id, users.second],
+        described_class::MASTER
+      )
+
+      expect(group.users).to include(users.first, users.second)
+    end
+  end
+
   describe 'notifications' do
     describe "#after_create" do
       it "sends email to user" do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 913d74645a7fd8ac04eb78a2358c0fa61e24c073..68f72f5c86ea9abf3bb9d9a725f3f8c1963f036e 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -1,27 +1,8 @@
-# == Schema Information
-#
-# Table name: members
-#
-#  id                 :integer          not null, primary key
-#  access_level       :integer          not null
-#  source_id          :integer          not null
-#  source_type        :string(255)      not null
-#  user_id            :integer
-#  notification_level :integer          not null
-#  type               :string(255)
-#  created_at         :datetime
-#  updated_at         :datetime
-#  created_by_id      :integer
-#  invite_email       :string(255)
-#  invite_token       :string(255)
-#  invite_accepted_at :datetime
-#
-
 require 'spec_helper'
 
 describe ProjectMember, models: true do
   describe 'associations' do
-    it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) }
+    it { is_expected.to belong_to(:project).with_foreign_key(:source_id) }
   end
 
   describe 'validations' do
@@ -34,6 +15,26 @@ describe ProjectMember, models: true do
     it { is_expected.to include_module(Gitlab::ShellAdapter) }
   end
 
+  describe '.access_level_roles' do
+    it 'returns Gitlab::Access.options' do
+      expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
+    end
+  end
+
+  describe '.add_user' do
+    context 'when called with the project owner' do
+      it 'adds the user as a member' do
+        project = create(:empty_project)
+
+        expect(project.users).not_to include(project.owner)
+
+        described_class.add_user(project, project.owner, :master, current_user: project.owner)
+
+        expect(project.users.reload).to include(project.owner)
+      end
+    end
+  end
+
   describe '#real_source_type' do
     subject { create(:project_member).real_source_type }
 
@@ -53,6 +54,17 @@ describe ProjectMember, models: true do
       master_todos
     end
 
+    it "creates an expired event when left due to expiry" do
+      expired = create(:project_member, project: project, expires_at: Time.now - 6.days)
+      expired.destroy
+      expect(Event.recent.first.action).to eq(Event::EXPIRED)
+    end
+
+    it "creates a left event when left due to leave" do
+      master.destroy
+      expect(Event.recent.first.action).to eq(Event::LEFT)
+    end
+
     it "destroys itself and delete associated todos" do
       expect(owner.user.todos.size).to eq(2)
       expect(master.user.todos.size).to eq(3)
@@ -69,11 +81,8 @@ describe ProjectMember, models: true do
     end
   end
 
-  describe :import_team do
+  describe '.import_team' do
     before do
-      @abilities = Six.new
-      @abilities << Ability
-
       @project_1 = create :project
       @project_2 = create :project
 
@@ -92,8 +101,8 @@ describe ProjectMember, models: true do
       it { expect(@project_2.users).to include(@user_1) }
       it { expect(@project_2.users).to include(@user_2) }
 
-      it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
-      it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
+      it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
+      it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
     end
 
     describe 'project 1 should not be changed' do
@@ -103,25 +112,21 @@ describe ProjectMember, models: true do
   end
 
   describe '.add_users_to_projects' do
-    before do
-      @project_1 = create :project
-      @project_2 = create :project
-
-      @user_1 = create :user
-      @user_2 = create :user
+    it 'adds the given users to the given projects' do
+      projects = create_list(:empty_project, 2)
+      users = create_list(:user, 2)
 
-      ProjectMember.add_users_to_projects(
-        [@project_1.id, @project_2.id],
-        [@user_1.id, @user_2.id],
-        ProjectMember::MASTER
-      )
-    end
+      described_class.add_users_to_projects(
+        [projects.first.id, projects.second],
+        [users.first.id, users.second],
+        described_class::MASTER)
 
-    it { expect(@project_1.users).to include(@user_1) }
-    it { expect(@project_1.users).to include(@user_2) }
+      expect(projects.first.users).to include(users.first)
+      expect(projects.first.users).to include(users.second)
 
-    it { expect(@project_2.users).to include(@user_1) }
-    it { expect(@project_2.users).to include(@user_2) }
+      expect(projects.second.users).to include(users.first)
+      expect(projects.second.users).to include(users.second)
+    end
   end
 
   describe '.truncate_teams' do
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..255db41cb19a364823a48026dbcd0925ed8ed285
--- /dev/null
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe MergeRequest::Metrics, models: true do
+  let(:project) { create(:project) }
+
+  subject { create(:merge_request, source_project: project) }
+
+  describe "when recording the default set of metrics on merge request save" do
+    it "records the merge time" do
+      time = Time.now
+      Timecop.freeze(time) { subject.mark_as_merged }
+      metrics = subject.metrics
+
+      expect(metrics).to be_present
+      expect(metrics.merged_at).to be_like_time(time)
+    end
+  end
+end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 29f7396f862822fcbaf7647353e8be12aacc9e5b..e500742404112e27d35e719e7a18911fa0c88d9c 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,6 +1,27 @@
 require 'spec_helper'
 
 describe MergeRequestDiff, models: true do
+  describe 'create new record' do
+    subject { create(:merge_request).merge_request_diff }
+
+    it { expect(subject).to be_valid }
+    it { expect(subject).to be_persisted }
+    it { expect(subject.commits.count).to eq(29) }
+    it { expect(subject.diffs.count).to eq(20) }
+    it { expect(subject.head_commit_sha).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
+    it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+    it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+  end
+
+  describe '#latest' do
+    let!(:mr) { create(:merge_request, :with_diffs) }
+    let!(:first_diff) { mr.merge_request_diff }
+    let!(:last_diff) { mr.create_merge_request_diff }
+
+    it { expect(last_diff.latest?).to be_truthy }
+    it { expect(first_diff.latest?).to be_falsey }
+  end
+
   describe '#diffs' do
     let(:mr) { create(:merge_request, :with_diffs) }
     let(:mr_diff) { mr.merge_request_diff }
@@ -23,6 +44,16 @@ describe MergeRequestDiff, models: true do
       end
     end
 
+    context 'when the raw diffs have invalid content' do
+      before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) }
+
+      it 'returns an empty DiffCollection' do
+        expect(mr_diff.raw_diffs.to_a).to be_empty
+        expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+        expect(mr_diff.raw_diffs).to be_empty
+      end
+    end
+
     context 'when the raw diffs exist' do
       it 'returns the diffs' do
         expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
@@ -44,4 +75,42 @@ describe MergeRequestDiff, models: true do
       end
     end
   end
+
+  describe '#commits_sha' do
+    shared_examples 'returning all commits SHA' do
+      it 'returns all commits SHA' do
+        commits_sha = subject.commits_sha
+
+        expect(commits_sha).to eq(subject.commits.map(&:sha))
+      end
+    end
+
+    context 'when commits were loaded' do
+      before do
+        subject.commits
+      end
+
+      it_behaves_like 'returning all commits SHA'
+    end
+
+    context 'when commits were not loaded' do
+      it_behaves_like 'returning all commits SHA'
+    end
+  end
+
+  describe '#compare_with' do
+    subject { create(:merge_request, source_branch: 'fix').merge_request_diff }
+
+    it 'delegates compare to the service' do
+      expect(CompareService).to receive(:new).and_call_original
+
+      subject.compare_with(nil)
+    end
+
+    it 'uses git diff A..B approach by default' do
+      diffs = subject.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
+
+      expect(diffs.size).to eq(3)
+    end
+  end
 end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 35a4418ebb3682dc91febc474f445e51692633b1..fb032a89d503fb14b409730bac8afe6840190c54 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -6,10 +6,10 @@ describe MergeRequest, models: true do
   subject { create(:merge_request) }
 
   describe 'associations' do
-    it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
-    it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
+    it { is_expected.to belong_to(:target_project).class_name('Project') }
+    it { is_expected.to belong_to(:source_project).class_name('Project') }
     it { is_expected.to belong_to(:merge_user).class_name("User") }
-    it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
+    it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
   end
 
   describe 'modules' do
@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#cache_merge_request_closes_issues!' do
+    before do
+      subject.project.team << [subject.author, :developer]
+      subject.target_branch = subject.project.default_branch
+    end
+
+    it 'caches closed issues' do
+      issue  = create :issue, project: subject.project
+      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+      allow(subject).to receive(:commits).and_return([commit])
+
+      expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+    end
+
+    it 'does not cache issues from external trackers' do
+      subject.project.update_attribute(:has_external_issue_tracker, true)
+      issue  = ExternalIssue.new('JIRA-123', subject.project)
+      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+      allow(subject).to receive(:commits).and_return([commit])
+
+      expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+    end
+  end
+
   describe '#source_branch_sha' do
     let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
 
@@ -159,7 +183,7 @@ describe MergeRequest, models: true do
 
     context 'when there are MR diffs' do
       it 'delegates to the MR diffs' do
-        merge_request.merge_request_diff = MergeRequestDiff.new
+        merge_request.save
 
         expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
 
@@ -287,6 +311,46 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe "#wipless_title" do
+    ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
+      it "removes the '#{wip_prefix}' prefix" do
+        wipless_title = subject.title
+        subject.title = "#{wip_prefix}#{subject.title}"
+
+        expect(subject.wipless_title).to eq wipless_title
+      end
+
+      it "is satisfies the #work_in_progress? method" do
+        subject.title = "#{wip_prefix}#{subject.title}"
+        subject.title = subject.wipless_title
+
+        expect(subject.work_in_progress?).to eq false
+      end
+    end
+  end
+
+  describe "#wip_title" do
+    it "adds the WIP: prefix to the title" do
+      wip_title = "WIP: #{subject.title}"
+
+      expect(subject.wip_title).to eq wip_title
+    end
+
+    it "does not add the WIP: prefix multiple times" do
+      wip_title = "WIP: #{subject.title}"
+      subject.title = subject.wip_title
+      subject.title = subject.wip_title
+
+      expect(subject.wip_title).to eq wip_title
+    end
+
+    it "is satisfies the #work_in_progress? method" do
+      subject.title = subject.wip_title
+
+      expect(subject.work_in_progress?).to eq true
+    end
+  end
+
   describe '#can_remove_source_branch?' do
     let(:user) { create(:user) }
     let(:user2) { create(:user) }
@@ -316,7 +380,7 @@ describe MergeRequest, models: true do
     end
 
     it "can be removed if the last commit is the head of the source branch" do
-      allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit)
+      allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
 
       expect(subject.can_remove_source_branch?(user)).to be_truthy
     end
@@ -328,6 +392,42 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#merge_commit_message' do
+    it 'includes merge information as the title' do
+      request = build(:merge_request, source_branch: 'source', target_branch: 'target')
+
+      expect(request.merge_commit_message)
+        .to match("Merge branch 'source' into 'target'\n\n")
+    end
+
+    it 'includes its title in the body' do
+      request = build(:merge_request, title: 'Remove all technical debt')
+
+      expect(request.merge_commit_message)
+        .to match("Remove all technical debt\n\n")
+    end
+
+    it 'includes its description in the body' do
+      request = build(:merge_request, description: 'By removing all code')
+
+      expect(request.merge_commit_message)
+        .to match("By removing all code\n\n")
+    end
+
+    it 'includes its reference in the body' do
+      request = build_stubbed(:merge_request)
+
+      expect(request.merge_commit_message)
+        .to match("See merge request #{request.to_reference}")
+    end
+
+    it 'excludes multiple linebreak runs when description is blank' do
+      request = build(:merge_request, title: 'Title', description: nil)
+
+      expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
+    end
+  end
+
   describe "#reset_merge_when_build_succeeds" do
     let(:merge_if_green) do
       create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
@@ -389,7 +489,7 @@ describe MergeRequest, models: true do
       subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
 
       it 'counts commits that are on target branch but not on source branch' do
-        expect(subject.diverged_commits_count).to eq(5)
+        expect(subject.diverged_commits_count).to eq(29)
       end
     end
 
@@ -397,7 +497,7 @@ describe MergeRequest, models: true do
       subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
 
       it 'counts commits that are on target branch but not on source branch' do
-        expect(subject.diverged_commits_count).to eq(5)
+        expect(subject.diverged_commits_count).to eq(29)
       end
     end
 
@@ -446,7 +546,7 @@ describe MergeRequest, models: true do
   end
 
   it_behaves_like 'an editable mentionable' do
-    subject { create(:merge_request) }
+    subject { create(:merge_request, :simple) }
 
     let(:backref_text) { "merge request #{subject.to_reference}" }
     let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
@@ -456,6 +556,20 @@ describe MergeRequest, models: true do
     subject { create :merge_request, :simple }
   end
 
+  describe '#commits_sha' do
+    let(:commit0) { double('commit0', sha: 'sha1') }
+    let(:commit1) { double('commit1', sha: 'sha2') }
+    let(:commit2) { double('commit2', sha: 'sha3') }
+
+    before do
+      allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
+    end
+
+    it 'returns sha of commits' do
+      expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+    end
+  end
+
   describe '#pipeline' do
     describe 'when the source project exists' do
       it 'returns the latest pipeline' do
@@ -463,8 +577,8 @@ describe MergeRequest, models: true do
 
         allow(subject).to receive(:diff_head_sha).and_return('123abc')
 
-        expect(subject.source_project).to receive(:pipeline).
-          with('123abc', 'master').
+        expect(subject.source_project).to receive(:pipeline_for).
+          with('master', '123abc').
           and_return(pipeline)
 
         expect(subject.pipeline).to eq(pipeline)
@@ -480,6 +594,105 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#all_pipelines' do
+    shared_examples 'returning pipelines with proper ordering' do
+      let!(:all_pipelines) do
+        subject.all_commits_sha.map do |sha|
+          create(:ci_empty_pipeline,
+                 project: subject.source_project,
+                 sha: sha,
+                 ref: subject.source_branch)
+        end
+      end
+
+      it 'returns all pipelines' do
+        expect(subject.all_pipelines).not_to be_empty
+        expect(subject.all_pipelines).to eq(all_pipelines.reverse)
+      end
+    end
+
+    context 'with single merge_request_diffs' do
+      it_behaves_like 'returning pipelines with proper ordering'
+    end
+
+    context 'with multiple irrelevant merge_request_diffs' do
+      before do
+        subject.update(target_branch: 'v1.0.0')
+      end
+
+      it_behaves_like 'returning pipelines with proper ordering'
+    end
+
+    context 'with unsaved merge request' do
+      subject { build(:merge_request) }
+
+      let!(:pipeline) do
+        create(:ci_empty_pipeline,
+               project: subject.project,
+               sha: subject.diff_head_sha,
+               ref: subject.source_branch)
+      end
+
+      it 'returns pipelines from diff_head_sha' do
+        expect(subject.all_pipelines).to contain_exactly(pipeline)
+      end
+    end
+  end
+
+  describe '#all_commits_sha' do
+    context 'when merge request is persisted' do
+      let(:all_commits_sha) do
+        subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
+      end
+
+      shared_examples 'returning all SHA' do
+        it 'returns all SHA from all merge_request_diffs' do
+          expect(subject.merge_request_diffs.size).to eq(2)
+          expect(subject.all_commits_sha).to eq(all_commits_sha)
+        end
+      end
+
+      context 'with a completely different branch' do
+        before do
+          subject.update(target_branch: 'v1.0.0')
+        end
+
+        it_behaves_like 'returning all SHA'
+      end
+
+      context 'with a branch having no difference' do
+        before do
+          subject.update(target_branch: 'v1.1.0')
+          subject.reload # make sure commits were not cached
+        end
+
+        it_behaves_like 'returning all SHA'
+      end
+    end
+
+    context 'when merge request is not persisted' do
+      context 'when compare commits are set in the service' do
+        let(:commit) { spy('commit') }
+
+        subject do
+          build(:merge_request, compare_commits: [commit, commit])
+        end
+
+        it 'returns commits from compare commits temporary data' do
+          expect(subject.all_commits_sha).to eq [commit, commit]
+        end
+      end
+
+      context 'when compare commits are not set in the service' do
+        subject { build(:merge_request) }
+
+        it 'returns array with diff head sha element only' do
+          expect(subject.all_commits_sha).to eq [subject.diff_head_sha]
+        end
+      end
+    end
+  end
+
   describe '#participants' do
     let(:project) { create(:project, :public) }
 
@@ -612,11 +825,8 @@ describe MergeRequest, models: true do
     end
 
     context 'when failed' do
-      before { allow(subject).to receive(:broken?) { false } }
-
-      context 'when project settings restrict to merge only if build succeeds and build failed' do
+      context 'when #mergeable_ci_state? is false' do
         before do
-          project.only_allow_merge_if_build_succeeds = true
           allow(subject).to receive(:mergeable_ci_state?) { false }
         end
 
@@ -624,6 +834,16 @@ describe MergeRequest, models: true do
           expect(subject.mergeable_state?).to be_falsey
         end
       end
+
+      context 'when #mergeable_discussions_state? is false' do
+        before do
+          allow(subject).to receive(:mergeable_discussions_state?) { false }
+        end
+
+        it 'returns false' do
+          expect(subject.mergeable_state?).to be_falsey
+        end
+      end
     end
   end
 
@@ -674,18 +894,109 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe '#mergeable_discussions_state?' do
+    let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
+
+    context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
+      let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+
+      context 'with all discussions resolved' do
+        before do
+          merge_request.discussions.each { |d| d.resolve!(merge_request.author) }
+        end
+
+        it 'returns true' do
+          expect(merge_request.mergeable_discussions_state?).to be_truthy
+        end
+      end
+
+      context 'with unresolved discussions' do
+        before do
+          merge_request.discussions.each(&:unresolve!)
+        end
+
+        it 'returns false' do
+          expect(merge_request.mergeable_discussions_state?).to be_falsey
+        end
+      end
+    end
+
+    context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
+      let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: false) }
+
+      context 'with unresolved discussions' do
+        before do
+          merge_request.discussions.each(&:unresolve!)
+        end
+
+        it 'returns true' do
+          expect(merge_request.mergeable_discussions_state?).to be_truthy
+        end
+      end
+    end
+  end
+
   describe "#environments" do
     let(:project)       { create(:project) }
-    let!(:environment)  { create(:environment, project: project) }
-    let!(:environment1) { create(:environment, project: project) }
-    let!(:environment2) { create(:environment, project: project) }
     let(:merge_request) { create(:merge_request, source_project: project) }
 
-    it 'selects deployed environments' do
-      create(:deployment, environment: environment, sha: project.commit('master').id)
-      create(:deployment, environment: environment1, sha: project.commit('feature').id)
+    context 'with multiple environments' do
+      let(:environments) { create_list(:environment, 3, project: project) }
 
-      expect(merge_request.environments).to eq [environment]
+      before do
+        create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
+        create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
+      end
+
+      it 'selects deployed environments' do
+        expect(merge_request.environments).to contain_exactly(environments.first)
+      end
+    end
+
+    context 'with environments on source project' do
+      let(:source_project) do
+        create(:project) do |fork_project|
+          fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+        end
+      end
+
+      let(:merge_request) do
+        create(:merge_request,
+               source_project: source_project, source_branch: 'feature',
+               target_project: project)
+      end
+
+      let(:source_environment) { create(:environment, project: source_project) }
+
+      before do
+        create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
+      end
+
+      it 'selects deployed environments' do
+        expect(merge_request.environments).to contain_exactly(source_environment)
+      end
+
+      context 'with environments on target project' do
+        let(:target_environment) { create(:environment, project: project) }
+
+        before do
+          create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
+        end
+
+        it 'selects deployed environments' do
+          expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+        end
+      end
+    end
+
+    context 'without a diff_head_commit' do
+      before do
+        expect(merge_request).to receive(:diff_head_commit).and_return(nil)
+      end
+
+      it 'returns an empty array' do
+        expect(merge_request.environments).to be_empty
+      end
     end
   end
 
@@ -694,12 +1005,15 @@ describe MergeRequest, models: true do
 
     let(:commit) { subject.project.commit(sample_commit.id) }
 
-    it "reloads the diff content" do
-      expect(subject.merge_request_diff).to receive(:reload_content)
-
+    it "does not change existing merge request diff" do
+      expect(subject.merge_request_diff).not_to receive(:save_git_content)
       subject.reload_diff
     end
 
+    it "creates new merge request diff" do
+      expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
+    end
+
     it "executs diff cache service" do
       expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
 
@@ -709,13 +1023,15 @@ describe MergeRequest, models: true do
     it "updates diff note positions" do
       old_diff_refs = subject.diff_refs
 
-      merge_request_diff = subject.merge_request_diff
-
       # Update merge_request_diff so that #diff_refs will return commit.diff_refs
-      allow(merge_request_diff).to receive(:reload_content) do
-        merge_request_diff.base_commit_sha = commit.parent_id
-        merge_request_diff.start_commit_sha = commit.parent_id
-        merge_request_diff.head_commit_sha = commit.sha
+      allow(subject).to receive(:create_merge_request_diff) do
+        subject.merge_request_diffs.create(
+          base_commit_sha: commit.parent_id,
+          start_commit_sha: commit.parent_id,
+          head_commit_sha: commit.sha
+        )
+
+        subject.merge_request_diff(true)
       end
 
       expect(Notes::DiffPositionUpdateService).to receive(:new).with(
@@ -725,14 +1041,31 @@ describe MergeRequest, models: true do
         new_diff_refs: commit.diff_refs,
         paths: note.position.paths
       ).and_call_original
-      expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
 
+      expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
       expect_any_instance_of(DiffNote).to receive(:save).once
 
       subject.reload_diff
     end
   end
 
+  describe '#branch_merge_base_commit' do
+    context 'source and target branch exist' do
+      it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+      it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
+    end
+
+    context 'when the target branch does not exist' do
+      before do
+        subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+      end
+
+      it 'returns nil' do
+        expect(subject.branch_merge_base_commit).to be_nil
+      end
+    end
+  end
+
   describe "#diff_sha_refs" do
     context "with diffs" do
       subject { create(:merge_request, :with_diffs) }
@@ -756,4 +1089,283 @@ describe MergeRequest, models: true do
       end
     end
   end
+
+  context "discussion status" do
+    let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+    let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+    let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+
+    before do
+      allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
+    end
+
+    describe "#discussions_resolvable?" do
+      context "when all discussions are unresolvable" do
+        before do
+          allow(first_discussion).to receive(:resolvable?).and_return(false)
+          allow(second_discussion).to receive(:resolvable?).and_return(false)
+          allow(third_discussion).to receive(:resolvable?).and_return(false)
+        end
+
+        it "returns false" do
+          expect(subject.discussions_resolvable?).to be false
+        end
+      end
+
+      context "when some discussions are unresolvable and some discussions are resolvable" do
+        before do
+          allow(first_discussion).to receive(:resolvable?).and_return(true)
+          allow(second_discussion).to receive(:resolvable?).and_return(false)
+          allow(third_discussion).to receive(:resolvable?).and_return(true)
+        end
+
+        it "returns true" do
+          expect(subject.discussions_resolvable?).to be true
+        end
+      end
+
+      context "when all discussions are resolvable" do
+        before do
+          allow(first_discussion).to receive(:resolvable?).and_return(true)
+          allow(second_discussion).to receive(:resolvable?).and_return(true)
+          allow(third_discussion).to receive(:resolvable?).and_return(true)
+        end
+
+        it "returns true" do
+          expect(subject.discussions_resolvable?).to be true
+        end
+      end
+    end
+
+    describe "#discussions_resolved?" do
+      context "when discussions are not resolvable" do
+        before do
+          allow(subject).to receive(:discussions_resolvable?).and_return(false)
+        end
+
+        it "returns false" do
+          expect(subject.discussions_resolved?).to be false
+        end
+      end
+
+      context "when discussions are resolvable" do
+        before do
+          allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+          allow(first_discussion).to receive(:resolvable?).and_return(true)
+          allow(second_discussion).to receive(:resolvable?).and_return(false)
+          allow(third_discussion).to receive(:resolvable?).and_return(true)
+        end
+
+        context "when all resolvable discussions are resolved" do
+          before do
+            allow(first_discussion).to receive(:resolved?).and_return(true)
+            allow(third_discussion).to receive(:resolved?).and_return(true)
+          end
+
+          it "returns true" do
+            expect(subject.discussions_resolved?).to be true
+          end
+        end
+
+        context "when some resolvable discussions are not resolved" do
+          before do
+            allow(first_discussion).to receive(:resolved?).and_return(true)
+            allow(third_discussion).to receive(:resolved?).and_return(false)
+          end
+
+          it "returns false" do
+            expect(subject.discussions_resolved?).to be false
+          end
+        end
+      end
+    end
+  end
+
+  describe '#conflicts_can_be_resolved_in_ui?' do
+    def create_merge_request(source_branch)
+      create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+        mr.mark_as_unmergeable
+      end
+    end
+
+    it 'returns a falsey value when the MR can be merged without conflicts' do
+      merge_request = create_merge_request('master')
+      merge_request.mark_as_mergeable
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+      merge_request = create_merge_request('master')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the MR has a missing ref after a force push' do
+      merge_request = create_merge_request('conflict-resolvable')
+      allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the MR does not support new diff notes' do
+      merge_request = create_merge_request('conflict-resolvable')
+      merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the conflicts contain a large file' do
+      merge_request = create_merge_request('conflict-too-large')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the conflicts contain a binary file' do
+      merge_request = create_merge_request('conflict-binary-file')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
+      merge_request = create_merge_request('conflict-missing-side')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+    end
+
+    it 'returns a truthy value when the conflicts are resolvable in the UI' do
+      merge_request = create_merge_request('conflict-resolvable')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+    end
+
+    it 'returns a truthy value when the conflicts have to be resolved in an editor' do
+      merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+    end
+  end
+
+  describe "#source_project_missing?" do
+    let(:project)      { create(:project) }
+    let(:fork_project) { create(:project, forked_from_project: project) }
+    let(:user)         { create(:user) }
+    let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+    context "when the fork exists" do
+      let(:merge_request) do
+        create(:merge_request,
+          source_project: fork_project,
+          target_project: project)
+      end
+
+      it { expect(merge_request.source_project_missing?).to be_falsey }
+    end
+
+    context "when the source project is the same as the target project" do
+      let(:merge_request) { create(:merge_request, source_project: project) }
+
+      it { expect(merge_request.source_project_missing?).to be_falsey }
+    end
+
+    context "when the fork does not exist" do
+      let(:merge_request) do
+        create(:merge_request,
+          source_project: fork_project,
+          target_project: project)
+      end
+
+      it "returns true" do
+        unlink_project.execute
+        merge_request.reload
+
+        expect(merge_request.source_project_missing?).to be_truthy
+      end
+    end
+  end
+
+  describe "#closed_without_fork?" do
+    let(:project)      { create(:project) }
+    let(:fork_project) { create(:project, forked_from_project: project) }
+    let(:user)         { create(:user) }
+    let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+    context "when the merge request is closed" do
+      let(:closed_merge_request) do
+        create(:closed_merge_request,
+          source_project: fork_project,
+          target_project: project)
+      end
+
+      it "returns false if the fork exist" do
+        expect(closed_merge_request.closed_without_fork?).to be_falsey
+      end
+
+      it "returns true if the fork does not exist" do
+        unlink_project.execute
+        closed_merge_request.reload
+
+        expect(closed_merge_request.closed_without_fork?).to be_truthy
+      end
+    end
+
+    context "when the merge request is open" do
+      let(:open_merge_request) do
+        create(:merge_request,
+          source_project: fork_project,
+          target_project: project)
+      end
+
+      it "returns false" do
+        expect(open_merge_request.closed_without_fork?).to be_falsey
+      end
+    end
+  end
+
+  describe '#reopenable?' do
+    context 'when the merge request is closed' do
+      it 'returns true' do
+        subject.close
+
+        expect(subject.reopenable?).to be_truthy
+      end
+
+      context 'forked project' do
+        let(:project)      { create(:project) }
+        let(:user)         { create(:user) }
+        let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+
+        let!(:merge_request) do
+          create(:closed_merge_request,
+            source_project: fork_project,
+            target_project: project)
+        end
+
+        it 'returns false if unforked' do
+          Projects::UnlinkForkService.new(fork_project, user).execute
+
+          expect(merge_request.reload.reopenable?).to be_falsey
+        end
+
+        it 'returns false if the source project is deleted' do
+          Projects::DestroyService.new(fork_project, user).execute
+
+          expect(merge_request.reload.reopenable?).to be_falsey
+        end
+
+        it 'returns false if the merge request is merged' do
+          merge_request.update_attributes(state: 'merged')
+
+          expect(merge_request.reload.reopenable?).to be_falsey
+        end
+      end
+    end
+
+    context 'when the merge request is opened' do
+      it 'returns false' do
+        expect(subject.reopenable?).to be_falsey
+      end
+    end
+  end
 end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d64d6cde2b517bc9b0547c8ca35ce2bbd72998ca..33fe22dd98cb8bc0415cdcb95703f9e616c3c14d 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -20,10 +20,10 @@ describe Milestone, models: true do
   let(:user) { create(:user) }
 
   describe "#title" do
-    let(:milestone) { create(:milestone, title: "<b>test</b>") }
+    let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
 
     it "sanitizes title" do
-      expect(milestone.title).to eq("test")
+      expect(milestone.title).to eq("foo & bar -> 2.2")
     end
   end
 
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 544920d18240b4b902f213b1fc3c5346f130eb6a..431b3e4435ff200042d61dc3afd95d5794bc4e65 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -114,6 +114,7 @@ describe Namespace, models: true do
 
     it "cleans the path and makes sure it's available" do
       expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
+      expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
     end
   end
 end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b76513d2a3c7cb040c1cec1a95149f5a8f1bd0c8
--- /dev/null
+++ b/spec/models/network/graph_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Network::Graph, models: true do
+  let(:project) { create(:project) }
+  let!(:note_on_commit) { create(:note_on_commit, project: project) }
+
+  it '#initialize' do
+    graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+
+    expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+  end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 53733d253f7f86c2861fe67c278850877ea52512..e6b6e7c06344d7c1de2dcf9f2b60d9aafff1bb5d 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe Note, models: true do
+  include RepoHelpers
+
   describe 'associations' do
     it { is_expected.to belong_to(:project) }
     it { is_expected.to belong_to(:noteable).touch(true) }
@@ -83,8 +85,6 @@ describe Note, models: true do
       @u1 = create(:user)
       @u2 = create(:user)
       @u3 = create(:user)
-      @abilities = Six.new
-      @abilities << Ability
     end
 
     describe 'read' do
@@ -93,9 +93,9 @@ describe Note, models: true do
         @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
       end
 
-      it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey }
-      it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy }
-      it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy }
+      it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey }
     end
 
     describe 'write' do
@@ -104,9 +104,9 @@ describe Note, models: true do
         @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
       end
 
-      it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey }
-      it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy }
-      it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy }
+      it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey }
     end
 
     describe 'admin' do
@@ -116,9 +116,9 @@ describe Note, models: true do
         @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER)
       end
 
-      it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey }
-      it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy }
-      it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
+      it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy }
+      it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey }
     end
   end
 
@@ -223,7 +223,7 @@ describe Note, models: true do
     let(:note) do
       create :note,
         noteable: ext_issue, project: ext_proj,
-        note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+        note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
         system: true
     end
 
@@ -267,4 +267,81 @@ describe Note, models: true do
       expect(note.participants).to include(note.author)
     end
   end
+
+  describe ".grouped_diff_discussions" do
+    let!(:merge_request) { create(:merge_request) }
+    let(:project) { merge_request.project }
+    let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+    let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+    let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+    let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+    let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+
+    let(:active_position2) do
+      Gitlab::Diff::Position.new(
+        old_path: "files/ruby/popen.rb",
+        new_path: "files/ruby/popen.rb",
+        old_line: 16,
+        new_line: 22,
+        diff_refs: merge_request.diff_refs
+      )
+    end
+
+    let(:outdated_position) do
+      Gitlab::Diff::Position.new(
+        old_path: "files/ruby/popen.rb",
+        new_path: "files/ruby/popen.rb",
+        old_line: nil,
+        new_line: 9,
+        diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
+      )
+    end
+
+    subject { merge_request.notes.grouped_diff_discussions }
+
+    it "includes active discussions" do
+      discussions = subject.values
+
+      expect(discussions.count).to eq(2)
+      expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+      expect(discussions.all?(&:active?)).to be true
+
+      expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+      expect(discussions.last.notes).to eq([active_diff_note3])
+    end
+
+    it "doesn't include outdated discussions" do
+      expect(subject.values.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+    end
+
+    it "groups the discussions by line code" do
+      expect(subject[active_diff_note1.line_code].id).to eq(active_diff_note1.discussion_id)
+      expect(subject[active_diff_note3.line_code].id).to eq(active_diff_note3.discussion_id)
+    end
+  end
+
+  describe "#discussion_id" do
+    let(:note) { create(:note) }
+
+    context "when it is newly created" do
+      it "has a discussion id" do
+        expect(note.discussion_id).not_to be_nil
+        expect(note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+
+    context "when it didn't store a discussion id before" do
+      before do
+        note.update_column(:discussion_id, nil)
+      end
+
+      it "has a discussion id" do
+        # The discussion_id is set in `after_initialize`, so `reload` won't work
+        reloaded_note = Note.find(note.id)
+
+        expect(reloaded_note.discussion_id).not_to be_nil
+        expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+      end
+    end
+  end
 end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a55d43ab2f9ac819a62e900b145385881d368d1d
--- /dev/null
+++ b/spec/models/project_feature_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe ProjectFeature do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  describe '#feature_available?' do
+    let(:features) { %w(issues wiki builds merge_requests snippets repository) }
+
+    context 'when features are disabled' do
+      it "returns false" do
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
+          expect(project.feature_available?(:issues, user)).to eq(false)
+        end
+      end
+    end
+
+    context 'when features are enabled only for team members' do
+      it "returns false when user is not a team member" do
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+          expect(project.feature_available?(:issues, user)).to eq(false)
+        end
+      end
+
+      it "returns true when user is a team member" do
+        project.team << [user, :developer]
+
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+          expect(project.feature_available?(:issues, user)).to eq(true)
+        end
+      end
+
+      it "returns true when user is a member of project group" do
+        group = create(:group)
+        project = create(:project, namespace: group)
+        group.add_developer(user)
+
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+          expect(project.feature_available?(:issues, user)).to eq(true)
+        end
+      end
+
+      it "returns true if user is an admin" do
+        user.update_attribute(:admin, true)
+
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+          expect(project.feature_available?(:issues, user)).to eq(true)
+        end
+      end
+    end
+
+    context 'when feature is enabled for everyone' do
+      it "returns true" do
+        features.each do |feature|
+          project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
+          expect(project.feature_available?(:issues, user)).to eq(true)
+        end
+      end
+    end
+  end
+
+  context 'repository related features' do
+    before do
+      project.project_feature.update_attributes(
+        merge_requests_access_level: ProjectFeature::DISABLED,
+        builds_access_level: ProjectFeature::DISABLED,
+        repository_access_level: ProjectFeature::PRIVATE
+      )
+    end
+
+    it "does not allow repository related features have higher level" do
+      features = %w(builds merge_requests)
+      project_feature = project.project_feature
+
+      features.each do |feature|
+        field = "#{feature}_access_level".to_sym
+        project_feature.update_attribute(field, ProjectFeature::ENABLED)
+        expect(project_feature.valid?).to be_falsy
+      end
+    end
+  end
+
+  describe '#*_enabled?' do
+    let(:features) { %w(wiki builds merge_requests) }
+
+    it "returns false when feature is disabled" do
+      features.each do |feature|
+        project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
+        expect(project.public_send("#{feature}_enabled?")).to eq(false)
+      end
+    end
+
+    it "returns true when feature is enabled only for team members" do
+      features.each do |feature|
+        project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
+        expect(project.public_send("#{feature}_enabled?")).to eq(true)
+      end
+    end
+
+    it "returns true when feature is enabled for everyone" do
+      features.each do |feature|
+        project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
+        expect(project.public_send("#{feature}_enabled?")).to eq(true)
+      end
+    end
+  end
+end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 2fa6715fcaf67a3cab146207861f0c82e3925ed9..c5ff1941378388bb6b07528319394543934cdac2 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -11,7 +11,7 @@ describe ProjectGroupLink do
 
     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_id) }
+    it { should validate_presence_of(:group) }
     it { should validate_presence_of(:group_access) }
   end
 end
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..18c9d449ee54ef30c870bb1992112cc993f5593b
--- /dev/null
+++ b/spec/models/project_label_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe ProjectLabel, models: true do
+  describe 'relationships' do
+    it { is_expected.to belong_to(:project) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:project) }
+
+    context 'validates if title must not exist at group level' do
+      let(:group) { create(:group, name: 'gitlab-org') }
+      let(:project) { create(:empty_project, group: group) }
+
+      before do
+        create(:group_label, group: group, title: 'Bug')
+      end
+
+      it 'returns error if title already exists at group level' do
+        label = described_class.new(project: project, title: 'Bug')
+
+        label.valid?
+
+        expect(label.errors[:title]).to include 'already exists at group level for gitlab-org. Please choose another one.'
+      end
+
+      it 'does not returns error if title does not exist at group level' do
+        label = described_class.new(project: project, title: 'Security')
+
+        label.valid?
+
+        expect(label.errors[:title]).to be_empty
+      end
+
+      it 'does not returns error if project does not belong to group' do
+        another_project = create(:empty_project)
+        label = described_class.new(project: another_project, title: 'Bug')
+
+        label.valid?
+
+        expect(label.errors[:title]).to be_empty
+      end
+
+      it 'does not returns error when title does not change' do
+        project_label = create(:label, project: project, name: 'Security')
+        create(:group_label, group: group, name: 'Security')
+        project_label.description = 'Security related stuff.'
+
+        project_label.valid?
+
+        expect(project_label.errors[:title]).to be_empty
+      end
+    end
+
+    context 'when attempting to add more than one priority to the project label' do
+      it 'returns error' do
+        subject.priorities.build
+        subject.priorities.build
+
+        subject.valid?
+
+        expect(subject.errors[:priorities]).to include 'Number of permitted priorities exceeded'
+      end
+    end
+  end
+
+  describe '#subject' do
+    it 'aliases project to subject' do
+      subject = described_class.new(project: build(:empty_project))
+
+      expect(subject.subject).to be(subject.project)
+    end
+  end
+
+  describe '#to_reference' do
+    let(:label) { create(:label) }
+
+    context 'using id' do
+      it 'returns a String reference to the object' do
+        expect(label.to_reference).to eq "~#{label.id}"
+      end
+    end
+
+    context 'using name' do
+      it 'returns a String reference to the object' do
+        expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+      end
+
+      it 'uses id when name contains double quote' do
+        label = create(:label, name: %q{"irony"})
+        expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+      end
+    end
+
+    context 'using invalid format' do
+      it 'raises error' do
+        expect { label.to_reference(format: :invalid) }
+          .to raise_error StandardError, /Unknown format/
+      end
+    end
+
+    context 'cross project reference' do
+      let(:project) { create(:project) }
+
+      context 'using name' do
+        it 'returns cross reference with label name' do
+          expect(label.to_reference(project, format: :name))
+            .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+        end
+      end
+
+      context 'using id' do
+        it 'returns cross reference with label id' do
+          expect(label.to_reference(project, format: :id))
+            .to eq %Q(#{label.project.to_reference}~#{label.id})
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
deleted file mode 100644
index 36379074ea0b522cd5d794972b730ae53c59b771..0000000000000000000000000000000000000000
--- a/spec/models/project_security_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-require 'spec_helper'
-
-describe Project, models: true do
-  describe 'authorization' do
-    before do
-      @p1 = create(:project)
-
-      @u1 = create(:user)
-      @u2 = create(:user)
-      @u3 = create(:user)
-      @u4 = @p1.owner
-
-      @abilities = Six.new
-      @abilities << Ability
-    end
-
-    let(:guest_actions) { Ability.project_guest_rules }
-    let(:report_actions) { Ability.project_report_rules }
-    let(:dev_actions) { Ability.project_dev_rules }
-    let(:master_actions) { Ability.project_master_rules }
-    let(:owner_actions) { Ability.project_owner_rules }
-
-    describe "Non member rules" do
-      it "denies for non-project users any actions" do
-        owner_actions.each do |action|
-          expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
-        end
-      end
-    end
-
-    describe "Guest Rules" do
-      before do
-        @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
-      end
-
-      it "allows for project user any guest actions" do
-        guest_actions.each do |action|
-          expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
-        end
-      end
-    end
-
-    describe "Report Rules" do
-      before do
-        @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
-      end
-
-      it "allows for project user any report actions" do
-        report_actions.each do |action|
-          expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
-        end
-      end
-    end
-
-    describe "Developer Rules" do
-      before do
-        @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
-        @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
-      end
-
-      it "denies for developer master-specific actions" do
-        [dev_actions - report_actions].each do |action|
-          expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
-        end
-      end
-
-      it "allows for project user any dev actions" do
-        dev_actions.each do |action|
-          expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
-        end
-      end
-    end
-
-    describe "Master Rules" do
-      before do
-        @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
-        @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
-      end
-
-      it "denies for developer master-specific actions" do
-        [master_actions - dev_actions].each do |action|
-          expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
-        end
-      end
-
-      it "allows for project user any master actions" do
-        master_actions.each do |action|
-          expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
-        end
-      end
-    end
-
-    describe "Owner Rules" do
-      before do
-        @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER)
-        @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
-      end
-
-      it "denies for masters admin-specific actions" do
-        [owner_actions - master_actions].each do |action|
-          expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
-        end
-      end
-
-      it "allows for project owner any admin actions" do
-        owner_actions.each do |action|
-          expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
-        end
-      end
-    end
-  end
-end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index dc702cfc42c5c88619937ba4d767c654ee3c9593..8e5145e824b09eb597a60fe1c58af265e55e2754 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe AsanaService, models: true do
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index d672d80156c91090c32a84dc7df732f44a406465..4c5acb7990be089af3a01435e30e929950a9b177 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe AssemblaService, models: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 9ae461f8c2d8befbb6042a3b49fec7aeb455e0f9..d7e1a4e3b6c51d965c0bd185a94e249d66ecec0a 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe BambooService, models: true do
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index a6d9717ccb52038af69880c59c1b4787cef9e834..739cc72b2ff7eb5e5207dcb7832c0fd91c16c49d 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe BugzillaService, models: true do
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 0866e1532dd2b03a9469a411ecc1792315662369..6f65beb79d0e8bb86cbe93d3a402aa20b1d4449e 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe BuildkiteService, models: true do
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index c76ae21421b3cbd58b3bb4e1f614bd8596aa3733..a3b9d084a755c21afd558fa02ffc023a995b63ca 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe CampfireService, models: true do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index ff976f8ec5932ff613ae4f51fc9e395b566c9155..63320931e769037394d25d2be8d5e68971f4ea74 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe CustomIssueTrackerService, models: true do
@@ -45,5 +25,21 @@ describe CustomIssueTrackerService, models: true do
       it { is_expected.not_to validate_presence_of(:issues_url) }
       it { is_expected.not_to validate_presence_of(:new_issue_url) }
     end
+
+    context 'title' do
+      let(:issue_tracker) { described_class.new(properties: {}) }
+
+      it 'sets a default title' do
+        issue_tracker.title = nil
+
+        expect(issue_tracker.title).to eq('Custom Issue Tracker')
+      end
+
+      it 'sets the custom title' do
+        issue_tracker.title = 'test title'
+
+        expect(issue_tracker.title).to eq('test title')
+      end
+    end
   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 8ef892259f260b8692b1b6f3319911d753371fe0..f13bb1e8adfe3d89fc88cd87a95ac609a1c36539 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe DroneCiService, models: true do
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index d7c5ea95d71af9f6fda4401a9503e4a8acc4def7..342d86aeca974e3d16e22de5e324760979c9f40d 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#  build_events          :boolean          default(FALSE), not null
-#
-
 require 'spec_helper'
 
 describe ExternalWikiService, models: true do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index d25570197567cadb8e05481741503fbc5a05f65a..d6db02d6e7666bd69a373baa4df0e9a863d92631 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe FlowdockService, models: true do
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 3d0b6c9816bdc45beed5a12d426767db5d7d8b61..529044d1d8bba734d988eec3caea9655f24867bf 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe GemnasiumService, models: true do
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 8ef79a17d502a05cd5a899eecd00bea3537f4363..652804fb44410ffe3d64ae932f8102a4b9374d95 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe GitlabIssueTrackerService, models: true do
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 34eafbe555d7234d75af619d96689db5bd991031..2da3a9cb09f446530092627b80fffe20e579aa67 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe HipchatService, models: true do
@@ -137,7 +117,7 @@ describe HipchatService, models: true do
     end
 
     context 'issue events' do
-      let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') }
+      let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') }
       let(:issue_service) { Issues::CreateService.new(project, user) }
       let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
 
@@ -155,12 +135,12 @@ describe HipchatService, models: true do
             "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \
             "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
             "<b>Awesome issue</b>" \
-            "<pre>please fix</pre>")
+            "<pre><strong>please</strong> fix</pre>")
       end
     end
 
     context 'merge request events' do
-      let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) }
+      let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) }
       let(:merge_service) { MergeRequests::CreateService.new(project, user) }
       let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
 
@@ -179,7 +159,7 @@ describe HipchatService, models: true do
             "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
             "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
             "<b>Awesome merge request</b>" \
-            "<pre>please fix</pre>")
+            "<pre><strong>please</strong> fix</pre>")
       end
     end
 
@@ -223,7 +203,7 @@ describe HipchatService, models: true do
         let(:merge_request_note) do
           create(:note_on_merge_request, noteable: merge_request,
                                          project: project,
-                                         note: "merge request note")
+                                         note: "merge request **note**")
         end
 
         it "calls Hipchat API for merge request comment events" do
@@ -242,7 +222,7 @@ describe HipchatService, models: true do
               "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
               "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
               "<b>#{title}</b>" \
-              "<pre>merge request note</pre>")
+              "<pre>merge request <strong>note</strong></pre>")
         end
       end
 
@@ -250,7 +230,7 @@ describe HipchatService, models: true do
         let(:issue) { create(:issue, project: project) }
         let(:issue_note) do
           create(:note_on_issue, noteable: issue, project: project,
-                                 note: "issue note")
+                                 note: "issue **note**")
         end
 
         it "calls Hipchat API for issue comment events" do
@@ -267,7 +247,7 @@ describe HipchatService, models: true do
               "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
               "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
               "<b>#{title}</b>" \
-              "<pre>issue note</pre>")
+              "<pre>issue <strong>note</strong></pre>")
         end
       end
 
@@ -303,7 +283,7 @@ describe HipchatService, models: true do
     context 'build events' do
       let(:pipeline) { create(:ci_empty_pipeline) }
       let(:build) { create(:ci_build, pipeline: pipeline) }
-      let(:data) { Gitlab::DataBuilder::Build.build(build) }
+      let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
 
       context 'for failed' do
         before { build.drop }
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index ffb17fd3259d3bbb4f0810d1619db131a3a5dead..f8c45b3756190612ec4e53395366f60d498d818f 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 require 'socket'
 require 'json'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 9037ca5cc2026513c9794247d51f8bd6c320a61b..05ee4a08391caa365e16e9212aabda503a5ce493 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,26 +1,8 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe JiraService, models: true do
+  include Gitlab::Routing.url_helpers
+
   describe "Associations" do
     it { is_expected.to belong_to :project }
     it { is_expected.to have_one :service_hook }
@@ -30,23 +12,59 @@ describe JiraService, models: true do
     context 'when service is active' do
       before { subject.active = true }
 
-      it { is_expected.to validate_presence_of(:api_url) }
-      it { is_expected.to validate_presence_of(:project_url) }
-      it { is_expected.to validate_presence_of(:issues_url) }
-      it { is_expected.to validate_presence_of(:new_issue_url) }
-      it_behaves_like 'issue tracker service URL attribute', :api_url
-      it_behaves_like 'issue tracker service URL attribute', :project_url
-      it_behaves_like 'issue tracker service URL attribute', :issues_url
-      it_behaves_like 'issue tracker service URL attribute', :new_issue_url
+      it { is_expected.to validate_presence_of(:url) }
+      it { is_expected.to validate_presence_of(:project_key) }
+      it_behaves_like 'issue tracker service URL attribute', :url
     end
 
     context 'when service is inactive' do
       before { subject.active = false }
 
-      it { is_expected.not_to validate_presence_of(:api_url) }
-      it { is_expected.not_to validate_presence_of(:project_url) }
-      it { is_expected.not_to validate_presence_of(:issues_url) }
-      it { is_expected.not_to validate_presence_of(:new_issue_url) }
+      it { is_expected.not_to validate_presence_of(:url) }
+    end
+  end
+
+  describe '#reference_pattern' do
+    it_behaves_like 'allows project key on reference pattern'
+
+    it 'does not allow # on the code' do
+      expect(subject.reference_pattern.match('#123')).to be_nil
+      expect(subject.reference_pattern.match('1#23#12')).to be_nil
+    end
+  end
+
+  describe '#can_test?' do
+    let(:jira_service) { described_class.new }
+
+    it 'returns false if username is blank' do
+      allow(jira_service).to receive_messages(
+        url: 'http://jira.example.com',
+        username: '',
+        password: '12345678'
+      )
+
+      expect(jira_service.can_test?).to be_falsy
+    end
+
+    it 'returns false if password is blank' do
+      allow(jira_service).to receive_messages(
+        url: 'http://jira.example.com',
+        username: 'tester',
+        password: ''
+      )
+
+      expect(jira_service.can_test?).to be_falsy
+    end
+
+    it 'returns true if password and username are present' do
+      jira_service = described_class.new
+      allow(jira_service).to receive_messages(
+        url: 'http://jira.example.com',
+        username: 'tester',
+        password: '12345678'
+      )
+
+      expect(jira_service.can_test?).to be_truthy
     end
   end
 
@@ -61,36 +79,72 @@ describe JiraService, models: true do
         project_id: project.id,
         project: project,
         service_hook: true,
-        project_url: 'http://jira.example.com',
+        url: 'http://jira.example.com',
         username: 'gitlab_jira_username',
-        password: 'gitlab_jira_password'
+        password: 'gitlab_jira_password',
+        project_key: 'GitLabProject'
       )
-      @jira_service.save # will build API URL, as api_url was not specified above
-      @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
-      # https://github.com/bblimke/webmock#request-with-basic-authentication
-      @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
-      @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
 
-      WebMock.stub_request(:post, @api_url)
+      @jira_service.save
+
+      project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
+      @project_url       = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject'
+      @transitions_url   = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
+      @comment_url       = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
+
+      WebMock.stub_request(:get, @project_url)
+      WebMock.stub_request(:get, project_issues_url)
+      WebMock.stub_request(:post, @transitions_url)
       WebMock.stub_request(:post, @comment_url)
     end
 
     it "calls JIRA API" do
-      @jira_service.execute(merge_request,
-                            ExternalIssue.new("JIRA-123", project))
+      @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
       expect(WebMock).to have_requested(:post, @comment_url).with(
         body: /Issue solved with/
       ).once
     end
 
+    it "references the GitLab commit/merge request" do
+      @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+      expect(WebMock).to have_requested(:post, @comment_url).with(
+        body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+      ).once
+    end
+
+    it "references the GitLab commit/merge request (relative URL)" do
+      stub_config_setting(relative_url_root: '/gitlab')
+      stub_config_setting(url: Settings.send(:build_gitlab_url))
+
+      allow(JiraService).to receive(:default_url_options) do
+        { script_name: '/gitlab' }
+      end
+
+      @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+      expect(WebMock).to have_requested(:post, @comment_url).with(
+        body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+      ).once
+    end
+
     it "calls the api with jira_issue_transition_id" do
       @jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
-      @jira_service.execute(merge_request,
-                            ExternalIssue.new("JIRA-123", project))
-      expect(WebMock).to have_requested(:post, @api_url).with(
+      @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+      expect(WebMock).to have_requested(:post, @transitions_url).with(
         body: /this-is-a-custom-id/
       ).once
     end
+
+    context "when testing" do
+      it "tries to get jira project" do
+        @jira_service.execute(nil)
+
+        expect(WebMock).to have_requested(:get, @project_url)
+      end
+    end
   end
 
   describe "Stored password invalidation" do
@@ -101,7 +155,7 @@ describe JiraService, models: true do
         @jira_service = JiraService.create!(
           project: create(:project),
           properties: {
-            api_url: 'http://jira.example.com/rest/api/2',
+            url: 'http://jira.example.com/rest/api/2',
             username: 'mic',
             password: "password"
           }
@@ -109,7 +163,7 @@ describe JiraService, models: true do
       end
 
       it "reset password if url changed" do
-        @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+        @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
         @jira_service.save
         expect(@jira_service.password).to be_nil
       end
@@ -121,16 +175,16 @@ describe JiraService, models: true do
       end
 
       it "does not reset password if new url is set together with password, even if it's the same password" do
-        @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+        @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
         @jira_service.password = 'password'
         @jira_service.save
         expect(@jira_service.password).to eq("password")
-        expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
+        expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
       end
 
       it "resets password if url changed, even if setter called multiple times" do
-        @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
-        @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
+        @jira_service.url = 'http://jira1.example.com/rest/api/2'
+        @jira_service.url = 'http://jira1.example.com/rest/api/2'
         @jira_service.save
         expect(@jira_service.password).to be_nil
       end
@@ -141,18 +195,18 @@ describe JiraService, models: true do
         @jira_service = JiraService.create(
           project: create(:project),
           properties: {
-            api_url: 'http://jira.example.com/rest/api/2',
+            url: 'http://jira.example.com/rest/api/2',
             username: 'mic'
           }
         )
       end
 
       it "saves password if new url is set together with password" do
-        @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+        @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
         @jira_service.password = 'password'
         @jira_service.save
         expect(@jira_service.password).to eq("password")
-        expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
+        expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
       end
     end
   end
@@ -163,9 +217,7 @@ describe JiraService, models: true do
         subject.active = true
       end
 
-      it { is_expected.to validate_presence_of :project_url }
-      it { is_expected.to validate_presence_of :issues_url }
-      it { is_expected.to validate_presence_of :new_issue_url }
+      it { is_expected.to validate_presence_of :url }
     end
   end
 
@@ -212,9 +264,7 @@ describe JiraService, models: true do
         settings = {
           "jira" => {
             "title" => "Jira",
-            "project_url" => "http://jira.sample/projects/project_a",
-            "issues_url" => "http://jira.sample/issues/:id",
-            "new_issue_url" => "http://jira.sample/projects/project_a/issues/new"
+            "url" => "http://jira.sample/projects/project_a"
           }
         }
         allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
@@ -226,9 +276,8 @@ describe JiraService, models: true do
       end
 
       it 'is prepopulated with the settings' do
-        expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
-        expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
-        expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
+        expect(@service.properties["title"]).to eq('Jira')
+        expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a')
       end
     end
   end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f56bceda44c9f2887a9e91dcf63f1a0cdaa0f41
--- /dev/null
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe PipelinesEmailService do
+  let(:pipeline) do
+    create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+  end
+
+  let(:project) { create(:project) }
+  let(:recipient) { 'test@gitlab.com' }
+
+  let(:data) do
+    Gitlab::DataBuilder::Pipeline.build(pipeline)
+  end
+
+  before do
+    reset_delivered_emails!
+  end
+
+  describe 'Validations' do
+    context 'when service is active' do
+      before do
+        subject.active = true
+      end
+
+      it { is_expected.to validate_presence_of(:recipients) }
+    end
+
+    context 'when service is inactive' do
+      before do
+        subject.active = false
+      end
+
+      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 do
+      project.team << [user, :developer]
+    end
+
+    it 'builds test data' do
+      data = subject.test_data(project, user)
+
+      expect(data[:object_kind]).to eq('pipeline')
+    end
+  end
+
+  shared_examples 'sending email' do
+    before do
+      perform_enqueued_jobs do
+        run
+      end
+    end
+
+    it 'sends email' do
+      should_only_email(double(notification_email: recipient), kind: :bcc)
+    end
+  end
+
+  shared_examples 'not sending email' do
+    before do
+      perform_enqueued_jobs do
+        run
+      end
+    end
+
+    it 'does not send email' do
+      should_not_email_anyone
+    end
+  end
+
+  describe '#test' do
+    def run
+      subject.test(data)
+    end
+
+    before do
+      subject.recipients = recipient
+    end
+
+    context 'when pipeline is failed' do
+      before do
+        data[:object_attributes][:status] = 'failed'
+        pipeline.update(status: 'failed')
+      end
+
+      it_behaves_like 'sending email'
+    end
+
+    context 'when pipeline is succeeded' do
+      before do
+        data[:object_attributes][:status] = 'success'
+        pipeline.update(status: 'success')
+      end
+
+      it_behaves_like 'sending email'
+    end
+  end
+
+  describe '#execute' do
+    def run
+      subject.execute(data)
+    end
+
+    context 'with recipients' do
+      before do
+        subject.recipients = recipient
+      end
+
+      context 'with failed pipeline' do
+        before do
+          data[:object_attributes][:status] = 'failed'
+          pipeline.update(status: 'failed')
+        end
+
+        it_behaves_like 'sending email'
+      end
+
+      context 'with succeeded pipeline' do
+        before do
+          data[:object_attributes][:status] = 'success'
+          pipeline.update(status: 'success')
+        end
+
+        it_behaves_like 'not sending email'
+      end
+
+      context 'with notify_only_broken_pipelines on' do
+        before do
+          subject.notify_only_broken_pipelines = true
+        end
+
+        context 'with failed pipeline' do
+          before do
+            data[:object_attributes][:status] = 'failed'
+            pipeline.update(status: 'failed')
+          end
+
+          it_behaves_like 'sending email'
+        end
+
+        context 'with succeeded pipeline' do
+          before do
+            data[:object_attributes][:status] = 'success'
+            pipeline.update(status: 'success')
+          end
+
+          it_behaves_like 'not sending email'
+        end
+      end
+    end
+
+    context 'with empty recipients list' do
+      before do
+        subject.recipients = ' ,, '
+      end
+
+      context 'with failed pipeline' do
+        before do
+          data[:object_attributes][:status] = 'failed'
+          pipeline.update(status: 'failed')
+        end
+
+        it_behaves_like 'not sending email'
+      end
+    end
+  end
+end
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index d098d988521188c8cb1694f52523d4b10216d595..45b2f1068bffb13e72b6ee8074238a9413269314 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe PivotaltrackerService, models: true do
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 5959c81577d93a6a8ec4ccfd3b1f73a56fccac6f..8fc92a9ab51aa4c5b25787f36e9dfa9bd300b2ef 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe PushoverService, models: true do
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 7d14f6e82806f6466dad3a06c1da7e2865f09358..0a7b237a0512e4d7aebb3478412c7719ea268826 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe RedmineService, models: true do
@@ -46,4 +26,12 @@ describe RedmineService, models: true do
       it { is_expected.not_to validate_presence_of(:new_issue_url) }
     end
   end
+
+  describe '#reference_pattern' do
+    it_behaves_like 'allows project key on reference pattern'
+
+    it 'does allow # on the reference' do
+      expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
+    end
+  end
 end
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
index 7fcfdf0eacdd76b11b3b4a0d53278384d7fb1ca7..452f4e2782c80568f71bffc76f8deeaa6438b0ed 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do
       tag: false,
 
       project_name: 'project_name',
-      project_url: 'somewhere.com',
+      project_url: 'example.gitlab.com',
 
       commit: {
         status: status,
@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do
     }
   end
 
-  context 'succeeded' do
+  let(:message) { build_message }
+
+  context 'build succeeded' do
     let(:status) { 'success' }
     let(:color) { 'good' }
     let(:duration) { 10 }
-    
+    let(:message) { build_message('passed') }
+
     it 'returns a message with information about succeeded build' do
-      message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
       expect(subject.pretext).to be_empty
       expect(subject.fallback).to eq(message)
       expect(subject.attachments).to eq([text: message, color: color])
     end
   end
 
-  context 'failed' do
+  context 'build failed' do
     let(:status) { 'failed' }
     let(:color) { 'danger' }
     let(:duration) { 10 }
 
     it 'returns a message with information about failed build' do
-      message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
       expect(subject.pretext).to be_empty
       expect(subject.fallback).to eq(message)
       expect(subject.attachments).to eq([text: message, color: color])
     end
-  end 
-  
-  describe '#seconds_name' do
-    let(:status) { 'failed' }
-    let(:color) { 'danger' }
-    let(:duration) { 1 }
+  end
 
-    it 'returns seconds as singular when there is only one' do
-      message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
-      expect(subject.pretext).to be_empty
-      expect(subject.fallback).to eq(message)
-      expect(subject.attachments).to eq([text: message, color: color])
-    end
+  def build_message(status_text = status)
+    "<example.gitlab.com|project_name>:" \
+    " Commit <example.gitlab.com/commit/" \
+    "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
+    " of <example.gitlab.com/commits/develop|develop> branch" \
+    " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
   end
 end
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index 0f8889bdf3c874ca933759ae1541226e185b84b5..98c36ec088dde79355b9a16943f194e68abbc285 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do
     {
       user: {
         name: 'Test User',
-        username: 'Test User'
+        username: 'test.user'
       },
       project_name: 'project_name',
       project_url: 'somewhere.com',
@@ -40,7 +40,7 @@ describe SlackService::IssueMessage, models: true do
   context 'open' do
     it 'returns a message regarding opening of issues' do
       expect(subject.pretext).to eq(
-        '<somewhere.com|[project_name>] Issue opened by Test User')
+        '<somewhere.com|[project_name>] Issue opened by test.user')
       expect(subject.attachments).to eq([
         {
           title: "#100 Issue title",
@@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do
 
     it 'returns a message regarding closing of issues' do
       expect(subject.pretext). to eq(
-        '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
+        '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
       expect(subject.attachments).to be_empty
     end
   end
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index 224c7ceabe881867792f93fe00f7b85085415386..c5c052d9af1b0e342bf0493f096f9ab7be7ad404 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do
     {
       user: {
           name: 'Test User',
-          username: 'Test User'
+          username: 'test.user'
       },
       project_name: 'project_name',
       project_url: 'somewhere.com',
@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
   context 'open' do
     it 'returns a message regarding opening of merge requests' do
       expect(subject.pretext).to eq(
-        'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\
+        'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
         'in <somewhere.com|project_name>: *Issue title*')
       expect(subject.attachments).to be_empty
     end
@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
     end
     it 'returns a message regarding closing of merge requests' do
       expect(subject.pretext).to eq(
-        'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\
+        'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
         'in <somewhere.com|project_name>: *Issue title*')
       expect(subject.attachments).to be_empty
     end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 41b93f08050833499baf90ca88a9ced8332f2399..38cfe4ad3e3c1ac7d803f7860573fe79dbd59729 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do
     @args = {
         user: {
             name: 'Test User',
-            username: 'username',
+            username: 'test.user',
             avatar_url: 'http://fakeavatar'
         },
         project_name: 'project_name',
@@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do
 
     it 'returns a message regarding notes on commits' do
       message = SlackService::NoteMessage.new(@args)
-      expect(message.pretext).to eq("Test User commented on " \
+      expect(message.pretext).to eq("test.user commented on " \
       "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
       "*Added a commit message*")
       expected_attachments = [
@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
 
     it 'returns a message regarding notes on a merge request' do
       message = SlackService::NoteMessage.new(@args)
-      expect(message.pretext).to eq("Test User commented on " \
+      expect(message.pretext).to eq("test.user commented on " \
       "<url|merge request !30> in <somewhere.com|project_name>: " \
       "*merge request title*")
       expected_attachments = [
@@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do
     it 'returns a message regarding notes on an issue' do
       message = SlackService::NoteMessage.new(@args)
       expect(message.pretext).to eq(
-        "Test User commented on " \
+        "test.user commented on " \
         "<url|issue #20> in <somewhere.com|project_name>: " \
         "*issue title*")
       expected_attachments = [
@@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do
 
     it 'returns a message regarding notes on a project snippet' do
       message = SlackService::NoteMessage.new(@args)
-      expect(message.pretext).to eq("Test User commented on " \
+      expect(message.pretext).to eq("test.user commented on " \
       "<url|snippet #5> in <somewhere.com|project_name>: " \
       "*snippet title*")
       expected_attachments = [
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..babb3909f5681eda53d10cb1a47a92bffec15d17
--- /dev/null
+++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe SlackService::PipelineMessage do
+  subject { SlackService::PipelineMessage.new(args) }
+
+  let(:args) do
+    {
+      object_attributes: {
+        id: 123,
+        sha: '97de212e80737a608d939f648d959671fb0a0142',
+        tag: false,
+        ref: 'develop',
+        status: status,
+        duration: duration
+      },
+      project: { path_with_namespace: 'project_name',
+                 web_url: 'example.gitlab.com' },
+      commit: { author_name: 'hacker' }
+    }
+  end
+
+  let(:message) { build_message }
+
+  context 'pipeline succeeded' do
+    let(:status) { 'success' }
+    let(:color) { 'good' }
+    let(:duration) { 10 }
+    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 'pipeline failed' do
+    let(:status) { 'failed' }
+    let(:color) { 'danger' }
+    let(:duration) { 10 }
+
+    it 'returns a message with information about failed build' do
+      expect(subject.pretext).to be_empty
+      expect(subject.fallback).to eq(message)
+      expect(subject.attachments).to eq([text: message, color: color])
+    end
+  end
+
+  def build_message(status_text = status)
+    "<example.gitlab.com|project_name>:" \
+    " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
+    " of <example.gitlab.com/commits/develop|develop> branch" \
+    " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+  end
+end
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
index cda9ee670b0dcd13f0b2538129960a9915090e5e..17cd05e24f1291e4765fc70ae839f11e0da82c6f 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/slack_service/push_message_spec.rb
@@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do
       before: 'before',
       project_name: 'project_name',
       ref: 'refs/heads/master',
-      user_name: 'user_name',
+      user_name: 'test.user',
       project_url: 'url'
     }
   end
@@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do
 
     it 'returns a message regarding pushes' do
       expect(subject.pretext).to eq(
-        'user_name pushed to branch <url/commits/master|master> of '\
+        'test.user pushed to branch <url/commits/master|master> of '\
         '<url|project_name> (<url/compare/before...after|Compare changes>)'
       )
       expect(subject.attachments).to eq([
@@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do
         before: Gitlab::Git::BLANK_SHA,
         project_name: 'project_name',
         ref: 'refs/tags/new_tag',
-        user_name: 'user_name',
+        user_name: 'test.user',
         project_url: 'url'
       }
     end
 
     it 'returns a message regarding pushes' do
-      expect(subject.pretext).to eq('user_name pushed new tag ' \
+      expect(subject.pretext).to eq('test.user pushed new tag ' \
        '<url/commits/new_tag|new_tag> to ' \
        '<url|project_name>')
       expect(subject.attachments).to be_empty
@@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do
 
     it 'returns a message regarding a new branch' do
       expect(subject.pretext).to eq(
-        'user_name pushed new branch <url/commits/master|master> to '\
+        'test.user pushed new branch <url/commits/master|master> to '\
         '<url|project_name>'
       )
       expect(subject.attachments).to be_empty
@@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do
 
     it 'returns a message regarding a removed branch' do
       expect(subject.pretext).to eq(
-        'user_name removed branch master from <url|project_name>'
+        'test.user removed branch master from <url|project_name>'
       )
       expect(subject.attachments).to be_empty
     end
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 13aea0b0600675a331eb883b889b298ce826f5d2..093911598b009e29a2a606c2231f0968e5cd82e1 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do
     {
       user: {
         name: 'Test User',
-        username: 'Test User'
+        username: 'test.user'
       },
       project_name: 'project_name',
       project_url: 'somewhere.com',
@@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do
 
       it 'returns a message that a new wiki page was created' do
         expect(subject.pretext).to eq(
-          'Test User created <url|wiki page> in <somewhere.com|project_name>: '\
+          'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
           '*Wiki page title*')
       end
     end
@@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do
 
       it 'returns a message that a wiki page was updated' do
         expect(subject.pretext).to eq(
-          'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\
+          'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
           '*Wiki page title*')
       end
     end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 28af68d13b49b64ff9fbb0076e528012f5e5d1a2..c07a70a806965975c637e5a877b7225811c95a71 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -1,26 +1,9 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe SlackService, models: true do
+  let(:slack) { SlackService.new }
+  let(:webhook_url) { 'https://example.gitlab.com/' }
+
   describe "Associations" do
     it { is_expected.to belong_to :project }
     it { is_expected.to have_one :service_hook }
@@ -42,15 +25,14 @@ describe SlackService, models: true do
   end
 
   describe "Execute" do
-    let(:slack)   { SlackService.new }
     let(:user)    { create(:user) }
     let(:project) { create(:project) }
+    let(:username) { 'slack_username' }
+    let(:channel)  { 'slack_channel' }
+
     let(:push_sample_data) do
       Gitlab::DataBuilder::Push.build_sample(project, user)
     end
-    let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
-    let(:username) { 'slack_username' }
-    let(:channel) { 'slack_channel' }
 
     before do
       allow(slack).to receive_messages(
@@ -212,10 +194,8 @@ describe SlackService, models: true do
   end
 
   describe "Note events" do
-    let(:slack)   { SlackService.new }
     let(:user) { create(:user) }
     let(:project) { create(:project, creator_id: user.id) }
-    let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
 
     before do
       allow(slack).to receive_messages(
@@ -285,4 +265,63 @@ describe SlackService, models: true do
       end
     end
   end
+
+  describe 'Pipeline events' do
+    let(:user) { create(:user) }
+    let(:project) { create(:project) }
+
+    let(:pipeline) do
+      create(:ci_pipeline,
+             project: project, status: status,
+             sha: project.commit.sha, ref: project.default_branch)
+    end
+
+    before do
+      allow(slack).to receive_messages(
+        project: project,
+        service_hook: true,
+        webhook: webhook_url
+      )
+    end
+
+    shared_examples 'call Slack API' do
+      before do
+        WebMock.stub_request(:post, webhook_url)
+      end
+
+      it 'calls Slack API for pipeline events' do
+        data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+        slack.execute(data)
+
+        expect(WebMock).to have_requested(:post, webhook_url).once
+      end
+    end
+
+    context 'with failed pipeline' do
+      let(:status) { 'failed' }
+
+      it_behaves_like 'call Slack API'
+    end
+
+    context 'with succeeded pipeline' do
+      let(:status) { 'success' }
+
+      context 'with default to notify_only_broken_pipelines' do
+        it 'does not call Slack API for pipeline events' do
+          data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+          result = slack.execute(data)
+
+          expect(result).to be_falsy
+        end
+      end
+
+      context 'with setting notify_only_broken_pipelines to false' do
+        before do
+          slack.notify_only_broken_pipelines = false
+        end
+
+        it_behaves_like 'call Slack API'
+      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 474715d24c3adbcd7cde80b017bba2aa72d9f5b5..f7e878844dcff4d87a01b589aa45242abccfb8a3 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,23 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-#  id                    :integer          not null, primary key
-#  type                  :string(255)
-#  title                 :string(255)
-#  project_id            :integer
-#  created_at            :datetime
-#  updated_at            :datetime
-#  active                :boolean          default(FALSE), not null
-#  properties            :text
-#  template              :boolean          default(FALSE)
-#  push_events           :boolean          default(TRUE)
-#  issues_events         :boolean          default(TRUE)
-#  merge_requests_events :boolean          default(TRUE)
-#  tag_push_events       :boolean          default(TRUE)
-#  note_events           :boolean          default(TRUE), not null
-#
-
 require 'spec_helper'
 
 describe TeamcityService, models: true do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0a32a486703588591690638370b59c6bf344c202..0810d06b50ff4d2dacc3cb57de9ae0a37fb10daa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ describe Project, models: true do
     it { is_expected.to belong_to(:namespace) }
     it { is_expected.to belong_to(:creator).class_name('User') }
     it { is_expected.to have_many(:users) }
+    it { is_expected.to have_many(:services) }
     it { is_expected.to have_many(:events).dependent(:destroy) }
     it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
     it { is_expected.to have_many(:issues).dependent(:destroy) }
@@ -23,6 +24,31 @@ describe Project, models: true do
     it { is_expected.to have_one(:slack_service).dependent(:destroy) }
     it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
     it { is_expected.to have_one(:asana_service).dependent(:destroy) }
+    it { is_expected.to have_many(:boards).dependent(:destroy) }
+    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(:irker_service).dependent(:destroy) }
+    it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
+    it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
+    it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
+    it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+    it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
+    it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
+    it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
+    it { is_expected.to have_one(:teamcity_service).dependent(:destroy) }
+    it { is_expected.to have_one(:jira_service).dependent(:destroy) }
+    it { is_expected.to have_one(:redmine_service).dependent(:destroy) }
+    it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) }
+    it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) }
+    it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
+    it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
+    it { is_expected.to have_one(:project_feature).dependent(:destroy) }
+    it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
+    it { is_expected.to have_one(:last_event).class_name('Event') }
+    it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
     it { is_expected.to have_many(:commit_statuses) }
     it { is_expected.to have_many(:pipelines) }
     it { is_expected.to have_many(:builds) }
@@ -30,12 +56,27 @@ describe Project, models: true do
     it { is_expected.to have_many(:runners) }
     it { is_expected.to have_many(:variables) }
     it { is_expected.to have_many(:triggers) }
+    it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
+    it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
     it { is_expected.to have_many(:environments).dependent(:destroy) }
     it { is_expected.to have_many(:deployments).dependent(:destroy) }
     it { is_expected.to have_many(:todos).dependent(:destroy) }
+    it { is_expected.to have_many(:releases).dependent(:destroy) }
+    it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) }
+    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) }
+
+    context 'after initialized' do
+      it "has a project_feature" do
+        project = FactoryGirl.build(:project)
+
+        expect(project.project_feature.present?).to be_present
+      end
+    end
 
     describe '#members & #requesters' do
-      let(:project) { create(:project) }
+      let(:project) { create(:project, :public) }
       let(:requester) { create(:user) }
       let(:developer) { create(:user) }
       before do
@@ -61,6 +102,15 @@ describe Project, models: true do
         end
       end
     end
+
+    describe '#boards' do
+      it 'raises an error when attempting to add more than one board to the project' do
+        subject.boards.build
+
+        expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
+        expect(subject.boards.size).to eq 1
+      end
+    end
   end
 
   describe 'modules' do
@@ -177,7 +227,7 @@ describe Project, models: true do
       expect(project.runners_token).not_to eq('')
     end
 
-    it 'does not set an random toke if one provided' do
+    it 'does not set an random token if one provided' do
       project = FactoryGirl.create :empty_project, runners_token: 'my-token'
       expect(project.runners_token).to eq('my-token')
     end
@@ -186,7 +236,6 @@ describe Project, models: true do
   describe 'Respond to' do
     it { is_expected.to respond_to(:url_to_repo) }
     it { is_expected.to respond_to(:repo_exists?) }
-    it { is_expected.to respond_to(:update_merge_requests) }
     it { is_expected.to respond_to(:execute_hooks) }
     it { is_expected.to respond_to(:owner) }
     it { is_expected.to respond_to(:path_with_namespace) }
@@ -256,8 +305,7 @@ describe Project, models: true do
       end
 
       it 'returns the address to create a new issue' do
-        token = user.authentication_token
-        address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab"
+        address = "p+#{project.path_with_namespace}+#{user.incoming_email_token}@gl.ab"
 
         expect(project.new_issue_address(user)).to eq(address)
       end
@@ -275,20 +323,24 @@ describe Project, models: true do
   end
 
   describe 'last_activity methods' do
-    let(:project) { create(:project) }
-    let(:last_event) { double(created_at: Time.now) }
+    let(:timestamp) { 2.hours.ago }
+    # last_activity_at gets set to created_at upon creation
+    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
 
     describe 'last_activity' do
       it 'alias last_activity to last_event' do
-        allow(project).to receive(:last_event).and_return(last_event)
+        last_event = create(:event, project: project)
+
         expect(project.last_activity).to eq(last_event)
       end
     end
 
     describe 'last_activity_date' do
       it 'returns the creation date of the project\'s last event if present' do
-        create(:event, project: project)
-        expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i)
+        new_event = create(:event, project: project, created_at: Time.now)
+
+        project.reload
+        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
       end
 
       it 'returns the project\'s last update date if it has no events' do
@@ -343,26 +395,6 @@ describe Project, models: true do
     end
   end
 
-  describe '#update_merge_requests' do
-    let(:project) { create(:project) }
-    let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
-    let(:key) { create(:key, user_id: project.owner.id) }
-    let(:prev_commit_id) { merge_request.commits.last.id }
-    let(:commit_id) { merge_request.commits.first.id }
-
-    it 'closes merge request if last commit from source branch was pushed to target branch' do
-      project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
-      merge_request.reload
-      expect(merge_request.merged?).to be_truthy
-    end
-
-    it 'updates merge request commits with new one if pushed to source branch' do
-      project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
-      merge_request.reload
-      expect(merge_request.diff_head_sha).to eq(commit_id)
-    end
-  end
-
   describe '.find_with_namespace' do
     context 'with namespace' do
       before do
@@ -484,7 +516,7 @@ describe Project, models: true do
   end
 
   describe '#cache_has_external_issue_tracker' do
-    let(:project) { create(:project) }
+    let(:project) { create(:project, has_external_issue_tracker: nil) }
 
     it 'stores true if there is any external_issue_tracker' do
       services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -505,6 +537,18 @@ describe Project, models: true do
     end
   end
 
+  describe '#has_wiki?' do
+    let(:no_wiki_project)       { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+    let(:wiki_enabled_project)  { create(:project) }
+    let(:external_wiki_project) { create(:project, has_external_wiki: true) }
+
+    it 'returns true if project is wiki enabled or has external wiki' do
+      expect(wiki_enabled_project).to have_wiki
+      expect(external_wiki_project).to have_wiki
+      expect(no_wiki_project).not_to have_wiki
+    end
+  end
+
   describe '#external_wiki' do
     let(:project) { create(:project) }
 
@@ -684,31 +728,43 @@ describe Project, models: true do
     end
   end
 
-  describe '#pipeline' do
-    let(:project) { create :project }
-    let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
-
-    subject { project.pipeline(pipeline.sha, 'master') }
+  describe '#pipeline_for' do
+    let(:project) { create(:project) }
+    let!(:pipeline) { create_pipeline }
 
-    it { is_expected.to eq(pipeline) }
+    shared_examples 'giving the correct pipeline' do
+      it { is_expected.to eq(pipeline) }
 
-    context 'return latest' do
-      let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
+      context 'return latest' do
+        let!(:pipeline2) { create_pipeline }
 
-      before do
-        pipeline
-        pipeline2
+        it { is_expected.to eq(pipeline2) }
       end
+    end
 
-      it { is_expected.to eq(pipeline2) }
+    context 'with explicit sha' do
+      subject { project.pipeline_for('master', pipeline.sha) }
+
+      it_behaves_like 'giving the correct pipeline'
+    end
+
+    context 'with implicit sha' do
+      subject { project.pipeline_for('master') }
+
+      it_behaves_like 'giving the correct pipeline'
+    end
+
+    def create_pipeline
+      create(:ci_pipeline,
+             project: project,
+             ref: 'master',
+             sha: project.commit('master').sha)
     end
   end
 
   describe '#builds_enabled' do
     let(:project) { create :project }
 
-    before { project.builds_enabled = true }
-
     subject { project.builds_enabled }
 
     it { expect(project.builds_enabled?).to be_truthy }
@@ -739,32 +795,22 @@ describe Project, models: true do
       end
 
       create(:note_on_commit, project: project2)
-    end
-
-    describe 'without an explicit start date' do
-      subject { described_class.trending.to_a }
 
-      it 'sorts Projects by the amount of notes in descending order' do
-        expect(subject).to eq([project1, project2])
-      end
+      TrendingProject.refresh!
     end
 
-    describe 'with an explicit start date' do
-      let(:date) { 2.months.ago }
+    subject { described_class.trending.to_a }
 
-      subject { described_class.trending(date).to_a }
+    it 'sorts projects by the amount of notes in descending order' do
+      expect(subject).to eq([project1, project2])
+    end
 
-      before do
-        2.times do
-          # Little fix for special issue related to Fractional Seconds support for MySQL.
-          # See: https://github.com/rails/rails/pull/14359/files
-          create(:note_on_commit, project: project2, created_at: date + 1)
-        end
+    it 'does not take system notes into account' do
+      10.times do
+        create(:note_on_commit, project: project2, system: true)
       end
 
-      it 'sorts Projects by the amount of notes in descending order' do
-        expect(subject).to eq([project2, project1])
-      end
+      expect(described_class.trending.to_a).to eq([project1, project2])
     end
   end
 
@@ -776,7 +822,7 @@ describe Project, models: true do
 
     describe 'when a user has access to a project' do
       before do
-        project.team.add_user(user, Gitlab::Access::MASTER)
+        project.add_user(user, Gitlab::Access::MASTER)
       end
 
       it { is_expected.to eq([project]) }
@@ -790,16 +836,19 @@ describe Project, models: true do
   context 'repository storage by default' do
     let(:project) { create(:empty_project) }
 
-    subject { project.repository_storage }
-
     before do
-      storages = { 'alternative_storage' => '/some/path' }
+      storages = {
+        'default' => 'tmp/tests/repositories',
+        'picked'  => 'tmp/tests/repositories',
+      }
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
-      stub_application_setting(repository_storage: 'alternative_storage')
-      allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(true)
     end
 
-    it { is_expected.to eq('alternative_storage') }
+    it 'picks storage from ApplicationSetting' do
+      expect_any_instance_of(ApplicationSetting).to receive(:pick_repository_storage).and_return('picked')
+
+      expect(project.repository_storage).to eq('picked')
+    end
   end
 
   context 'shared runners by default' do
@@ -1360,6 +1409,68 @@ describe Project, models: true do
     end
   end
 
+  describe '#lfs_enabled?' do
+    let(:project) { create(:project) }
+
+    shared_examples 'project overrides group' do
+      it 'returns true when enabled in project' do
+        project.update_attribute(:lfs_enabled, true)
+
+        expect(project.lfs_enabled?).to be_truthy
+      end
+
+      it 'returns false when disabled in project' do
+        project.update_attribute(:lfs_enabled, false)
+
+        expect(project.lfs_enabled?).to be_falsey
+      end
+
+      it 'returns the value from the namespace, when no value is set in project' do
+        expect(project.lfs_enabled?).to eq(project.namespace.lfs_enabled?)
+      end
+    end
+
+    context 'LFS disabled in group' do
+      before do
+        project.namespace.update_attribute(:lfs_enabled, false)
+        enable_lfs
+      end
+
+      it_behaves_like 'project overrides group'
+    end
+
+    context 'LFS enabled in group' do
+      before do
+        project.namespace.update_attribute(:lfs_enabled, true)
+        enable_lfs
+      end
+
+      it_behaves_like 'project overrides group'
+    end
+
+    describe 'LFS disabled globally' do
+      shared_examples 'it always returns false' do
+        it do
+          expect(project.lfs_enabled?).to be_falsey
+          expect(project.namespace.lfs_enabled?).to be_falsey
+        end
+      end
+
+      context 'when no values are set' do
+        it_behaves_like 'it always returns false'
+      end
+
+      context 'when all values are set to true' do
+        before do
+          project.namespace.update_attribute(:lfs_enabled, true)
+          project.update_attribute(:lfs_enabled, true)
+        end
+
+        it_behaves_like 'it always returns false'
+      end
+    end
+  end
+
   describe '.where_paths_in' do
     context 'without any paths' do
       it 'returns an empty relation' do
@@ -1441,4 +1552,132 @@ describe Project, models: true do
       expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
     end
   end
+
+  describe 'change_head' do
+    let(:project) { create(:project) }
+
+    it 'calls the before_change_head method' do
+      expect(project.repository).to receive(:before_change_head)
+      project.change_head(project.default_branch)
+    end
+
+    it 'creates the new reference with rugged' do
+      expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+                                                                            "refs/heads/#{project.default_branch}",
+                                                                            force: true)
+      project.change_head(project.default_branch)
+    end
+
+    it 'copies the gitattributes' do
+      expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
+      project.change_head(project.default_branch)
+    end
+
+    it 'expires the avatar cache' do
+      expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+      project.change_head(project.default_branch)
+    end
+
+    it 'reloads the default branch' do
+      expect(project).to receive(:reload_default_branch)
+      project.change_head(project.default_branch)
+    end
+  end
+
+  describe '#pushes_since_gc' do
+    let(:project) { create(:project) }
+
+    after do
+      project.reset_pushes_since_gc
+    end
+
+    context 'without any pushes' do
+      it 'returns 0' do
+        expect(project.pushes_since_gc).to eq(0)
+      end
+    end
+
+    context 'with a number of pushes' do
+      it 'returns the number of pushes' do
+        3.times { project.increment_pushes_since_gc }
+
+        expect(project.pushes_since_gc).to eq(3)
+      end
+    end
+  end
+
+  describe '#increment_pushes_since_gc' do
+    let(:project) { create(:project) }
+
+    after do
+      project.reset_pushes_since_gc
+    end
+
+    it 'increments the number of pushes since the last GC' do
+      3.times { project.increment_pushes_since_gc }
+
+      expect(project.pushes_since_gc).to eq(3)
+    end
+  end
+
+  describe '#reset_pushes_since_gc' do
+    let(:project) { create(:project) }
+
+    after do
+      project.reset_pushes_since_gc
+    end
+
+    it 'resets the number of pushes since the last GC' do
+      3.times { project.increment_pushes_since_gc }
+
+      project.reset_pushes_since_gc
+
+      expect(project.pushes_since_gc).to eq(0)
+    end
+  end
+
+  describe '#environments_for' do
+    let(:project) { create(:project) }
+    let(:environment) { create(:environment, project: project) }
+
+    context 'tagged deployment' do
+      before do
+        create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+      end
+
+      it 'returns environment when with_tags is set' do
+        expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+      end
+
+      it 'does not return environment when no with_tags is set' do
+        expect(project.environments_for('master', project.commit)).to be_empty
+      end
+
+      it 'does not return environment when commit is not part of deployment' do
+        expect(project.environments_for('master', project.commit('feature'))).to be_empty
+      end
+    end
+
+    context 'branch deployment' do
+      before do
+        create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+      end
+
+      it 'returns environment when ref is set' do
+        expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+      end
+
+      it 'does not environment when ref is different' do
+        expect(project.environments_for('feature', project.commit)).to be_empty
+      end
+
+      it 'does not return environment when commit is not part of deployment' do
+        expect(project.environments_for('master', project.commit('feature'))).to be_empty
+      end
+    end
+  end
+
+  def enable_lfs
+    allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+  end
 end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 5eaf0d3b7a6c5f1b823647f744bc6e84fdfb9066..e0f2dadf1896eff40b804c69666de1dad838b592 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -73,9 +73,71 @@ describe ProjectTeam, models: true do
     end
   end
 
-  describe '#find_member' do
+  describe '#fetch_members' do
     context 'personal project' do
       let(:project) { create(:empty_project) }
+
+      it 'returns project members' do
+        user = create(:user)
+        project.team << [user, :guest]
+
+        expect(project.team.members).to contain_exactly(user)
+      end
+
+      it 'returns project members of a specified level' do
+        user = create(:user)
+        project.team << [user, :reporter]
+
+        expect(project.team.guests).to be_empty
+        expect(project.team.reporters).to contain_exactly(user)
+      end
+
+      it 'returns invited members of a group' do
+        group_member = create(:group_member)
+
+        project.project_group_links.create!(
+          group: group_member.group,
+          group_access: Gitlab::Access::GUEST
+        )
+
+        expect(project.team.members).to contain_exactly(group_member.user)
+      end
+
+      it 'returns invited members of a group of a specified level' do
+        group_member = create(:group_member)
+
+        project.project_group_links.create!(
+          group: group_member.group,
+          group_access: Gitlab::Access::REPORTER
+        )
+
+        expect(project.team.guests).to be_empty
+        expect(project.team.reporters).to contain_exactly(group_member.user)
+      end
+    end
+
+    context 'group project' do
+      let(:group) { create(:group) }
+      let(:project) { create(:empty_project, group: group) }
+
+      it 'returns project members' do
+        group_member = create(:group_member, group: group)
+
+        expect(project.team.members).to contain_exactly(group_member.user)
+      end
+
+      it 'returns project members of a specified level' do
+        group_member = create(:group_member, :reporter, group: group)
+
+        expect(project.team.guests).to be_empty
+        expect(project.team.reporters).to contain_exactly(group_member.user)
+      end
+    end
+  end
+
+  describe '#find_member' do
+    context 'personal project' do
+      let(:project) { create(:empty_project, :public) }
       let(:requester) { create(:user) }
 
       before do
@@ -138,7 +200,7 @@ describe ProjectTeam, models: true do
     let(:requester) { create(:user) }
 
     context 'personal project' do
-      let(:project) { create(:empty_project) }
+      let(:project) { create(:empty_project, :public) }
 
       context 'when project is not shared with group' do
         before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f7dbfd712cc4525a7a87afb14aa82375226f3077..fe26b4ac18cc28b6f7fc867c7b30cc8f100a11fb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -7,16 +7,34 @@ describe Repository, models: true do
   let(:project) { create(:project) }
   let(:repository) { project.repository }
   let(:user) { create(:user) }
+
   let(:commit_options) do
     author = repository.user_to_committer(user)
     { message: 'Test message', committer: author, author: author }
   end
+
   let(:merge_commit) do
     merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
     merge_commit_id = repository.merge(user, merge_request, commit_options)
     repository.commit(merge_commit_id)
   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.")
+  # This seems to be what git does under the hood. For example, this commit:
+  #
+  # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+  #
+  # results in this:
+  #
+  # $ git show --pretty
+  # ...
+  # Author: Foo Sr <foo@example.com>
+  # ...
+  let(:author_name) { FFaker::Name.name.chomp("\.") }
+
   describe '#branch_names_contains' do
     subject { repository.branch_names_contains(sample_commit.id) }
 
@@ -50,8 +68,8 @@ describe Repository, models: true do
           double_first = double(committed_date: Time.now)
           double_last = double(committed_date: Time.now - 1.second)
 
-          allow(tag_a).to receive(:target).and_return(double_first)
-          allow(tag_b).to receive(:target).and_return(double_last)
+          allow(tag_a).to receive(:dereferenced_target).and_return(double_first)
+          allow(tag_b).to receive(:dereferenced_target).and_return(double_last)
           allow(repository).to receive(:tags).and_return([tag_a, tag_b])
         end
 
@@ -65,8 +83,8 @@ describe Repository, models: true do
           double_first = double(committed_date: Time.now - 1.second)
           double_last = double(committed_date: Time.now)
 
-          allow(tag_a).to receive(:target).and_return(double_last)
-          allow(tag_b).to receive(:target).and_return(double_first)
+          allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
+          allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
           allow(repository).to receive(:tags).and_return([tag_a, tag_b])
         end
 
@@ -75,6 +93,46 @@ describe Repository, models: true do
     end
   end
 
+  describe '#ref_name_for_sha' do
+    context 'ref found' do
+      it 'returns the ref' do
+        allow_any_instance_of(Gitlab::Popen).to receive(:popen).
+          and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0])
+
+        expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
+      end
+    end
+
+    context 'ref not found' do
+      it 'returns nil' do
+        allow_any_instance_of(Gitlab::Popen).to receive(:popen).
+          and_return(["", 0])
+
+        expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil
+      end
+    end
+  end
+
+  describe '#ref_exists?' do
+    context 'when ref exists' do
+      it 'returns true' do
+        expect(repository.ref_exists?('refs/heads/master')).to be true
+      end
+    end
+
+    context 'when ref does not exist' do
+      it 'returns false' do
+        expect(repository.ref_exists?('refs/heads/non-existent')).to be false
+      end
+    end
+
+    context 'when ref format is incorrect' do
+      it 'returns false' do
+        expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
+      end
+    end
+  end
+
   describe '#last_commit_for_path' do
     subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
 
@@ -82,12 +140,20 @@ describe Repository, models: true do
   end
 
   describe '#find_commits_by_message' do
-    subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
+    it 'returns commits with messages containing a given string' do
+      commit_ids = repository.find_commits_by_message('submodule').map(&:id)
+
+      expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+      expect(commit_ids).to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+      expect(commit_ids).to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660')
+      expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+    end
+
+    it 'is case insensitive' do
+      commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
 
-    it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
-    it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
-    it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') }
-    it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+      expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+    end
   end
 
   describe '#blob_at' do
@@ -99,11 +165,30 @@ describe Repository, models: true do
   end
 
   describe '#merged_to_root_ref?' do
-    context 'merged branch' do
+    context 'merged branch without ff' do
+      subject { repository.merged_to_root_ref?('branch-merged') }
+
+      it { is_expected.to be_truthy }
+    end
+
+    # If the HEAD was ff then it will be false
+    context 'merged with ff' do
       subject { repository.merged_to_root_ref?('improve/awesome') }
 
       it { is_expected.to be_truthy }
     end
+
+    context 'not merged branch' do
+      subject { repository.merged_to_root_ref?('not-merged-branch') }
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'default branch' do
+      subject { repository.merged_to_root_ref?('master') }
+
+      it { is_expected.to be_falsey }
+    end
   end
 
   describe '#can_be_merged?' do
@@ -132,7 +217,60 @@ describe Repository, models: true do
     end
   end
 
-  describe :commit_file do
+  describe '#commit' do
+    context 'when ref exists' do
+      it 'returns commit object' do
+        expect(repository.commit('master'))
+          .to be_an_instance_of Commit
+      end
+    end
+
+    context 'when ref does not exist' do
+      it 'returns nil' do
+        expect(repository.commit('non-existent-ref')).to be_nil
+      end
+    end
+
+    context 'when ref is not valid' do
+      context 'when preceding tree element exists' do
+        it 'returns nil' do
+          expect(repository.commit('master:ref')).to be_nil
+        end
+      end
+
+      context 'when preceding tree element does not exist' do
+        it 'returns nil' do
+          expect(repository.commit('non-existent:ref')).to be_nil
+        end
+      end
+    end
+  end
+
+  describe "#commit_dir" do
+    it "commits a change that creates a new directory" do
+      expect do
+        repository.commit_dir(user, 'newdir', 'Create newdir', 'master')
+      end.to change { repository.commits('master').count }.by(1)
+
+      newdir = repository.tree('master', 'newdir')
+      expect(newdir.path).to eq('newdir')
+    end
+
+    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", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+        end.to change { repository.commits('master').count }.by(1)
+
+        last_commit = repository.commit
+
+        expect(last_commit.author_email).to eq(author_email)
+        expect(last_commit.author_name).to eq(author_name)
+      end
+    end
+  end
+
+  describe "#commit_file" do
     it 'commits change to a file successfully' do
       expect do
         repository.commit_file(user, 'CHANGELOG', 'Changelog!',
@@ -144,9 +282,23 @@ describe Repository, models: true do
 
       expect(blob.data).to eq('Changelog!')
     end
+
+    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!', 'Add README',
+                                'master', true, author_email: author_email, author_name: author_name)
+        end.to change { repository.commits('master').count }.by(1)
+
+        last_commit = repository.commit
+
+        expect(last_commit.author_email).to eq(author_email)
+        expect(last_commit.author_name).to eq(author_name)
+      end
+    end
   end
 
-  describe :update_file do
+  describe "#update_file" do
     it 'updates filename successfully' do
       expect do
         repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -160,6 +312,85 @@ describe Repository, models: true do
       expect(files).not_to include('LICENSE')
       expect(files).to include('NEWLICENSE')
     end
+
+    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!', 'Add README', 'master', true)
+
+        expect do
+          repository.update_file(user, 'README', "Updated README!",
+                                branch: 'master',
+                                previous_path: 'README',
+                                message: 'Update README',
+                                author_email: author_email,
+                                author_name: author_name)
+        end.to change { repository.commits('master').count }.by(1)
+
+        last_commit = repository.commit
+
+        expect(last_commit.author_email).to eq(author_email)
+        expect(last_commit.author_name).to eq(author_name)
+      end
+    end
+  end
+
+  describe "#remove_file" do
+    it 'removes file successfully' do
+      repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+      expect do
+        repository.remove_file(user, "README", "Remove README", 'master')
+      end.to change { repository.commits('master').count }.by(1)
+
+      expect(repository.blob_at('master', 'README')).to be_nil
+    end
+
+    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!', 'Add README', 'master', true)
+
+        expect do
+          repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+        end.to change { repository.commits('master').count }.by(1)
+
+        last_commit = repository.commit
+
+        expect(last_commit.author_email).to eq(author_email)
+        expect(last_commit.author_name).to eq(author_name)
+      end
+    end
+  end
+
+  describe '#get_committer_and_author' do
+    it 'returns the committer and author data' do
+      options = repository.get_committer_and_author(user)
+      expect(options[:committer][:email]).to eq(user.email)
+      expect(options[:author][:email]).to eq(user.email)
+    end
+
+    context 'when the email/name are given' do
+      it 'returns an object containing the email/name' do
+        options = repository.get_committer_and_author(user, email: author_email, name: author_name)
+        expect(options[:author][:email]).to eq(author_email)
+        expect(options[:author][:name]).to eq(author_name)
+      end
+    end
+
+    context 'when the email is given but the name is not' do
+      it 'returns the committer as the author' do
+        options = repository.get_committer_and_author(user, email: author_email)
+        expect(options[:author][:email]).to eq(user.email)
+        expect(options[:author][:name]).to eq(user.name)
+      end
+    end
+
+    context 'when the name is given but the email is not' do
+      it 'returns nil' do
+        options = repository.get_committer_and_author(user, name: author_name)
+        expect(options[:author][:email]).to eq(user.email)
+        expect(options[:author][:name]).to eq(user.name)
+      end
+    end
   end
 
   describe "search_files" do
@@ -180,37 +411,34 @@ describe Repository, models: true do
       expect(results.first).not_to start_with('fatal:')
     end
 
-    describe 'result' do
-      subject { results.first }
+    it 'properly handles when query is not present' do
+      results = repository.search_files('', 'master')
 
-      it { is_expected.to be_an String }
-      it { expect(subject.lines[2]).to eq("master:CHANGELOG:188:  - Feature: Replace teams with group membership\n") }
+      expect(results).to match_array([])
     end
 
-    describe 'parsing result' do
-      subject { repository.parse_search_result(search_result) }
-      let(:search_result) { results.first }
+    it 'properly handles query when repo is empty' do
+      repository = create(:empty_project).repository
+      results = repository.search_files('test', 'master')
 
-      it { is_expected.to be_an OpenStruct }
-      it { expect(subject.filename).to eq('CHANGELOG') }
-      it { expect(subject.basename).to eq('CHANGELOG') }
-      it { expect(subject.ref).to eq('master') }
-      it { expect(subject.startline).to eq(186) }
-      it { expect(subject.data.lines[2]).to eq("  - Feature: Replace teams with group membership\n") }
+      expect(results).to match_array([])
+    end
 
-      context "when filename has extension" do
-        let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+    describe 'result' do
+      subject { results.first }
 
-        it { expect(subject.filename).to eq('CONTRIBUTE.md') }
-        it { expect(subject.basename).to eq('CONTRIBUTE') }
-      end
+      it { is_expected.to be_an String }
+      it { expect(subject.lines[2]).to eq("master:CHANGELOG:190:  - Feature: Replace teams with group membership\n") }
+    end
+  end
 
-      context "when file under directory" do
-        let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+  describe '#create_ref' do
+    it 'redirects the call to fetch_ref' do
+      ref, ref_path = '1', '2'
 
-        it { expect(subject.filename).to eq('a/b/c.md') }
-        it { expect(subject.basename).to eq('a/b/c') }
-      end
+      expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
+
+      repository.create_ref(ref, ref_path)
     end
   end
 
@@ -382,6 +610,24 @@ describe Repository, models: true do
     end
   end
 
+  describe '#find_branch' do
+    it 'loads a branch with a fresh repo' do
+      expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original
+
+      2.times do
+        expect(repository.find_branch('feature')).not_to be_nil
+      end
+    end
+
+    it 'loads a branch with a cached repo' do
+      expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original
+
+      2.times do
+        expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
+      end
+    end
+  end
+
   describe '#rm_branch' do
     let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
     let(:blank_sha) { '0000000000000000000000000000000000000000' }
@@ -423,43 +669,77 @@ describe Repository, models: true do
     end
   end
 
-  describe '#commit_with_hooks' do
+  describe '#update_branch_with_hooks' do
     let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+    let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
 
     context 'when pre hooks were successful' do
       before do
         expect_any_instance_of(GitHooksService).to receive(:execute).
-          with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature').
+          with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
           and_yield.and_return(true)
       end
 
       it 'runs without errors' do
         expect do
-          repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+          repository.update_branch_with_hooks(user, 'feature') { new_rev }
         end.not_to raise_error
       end
 
       it 'ensures the autocrlf Git option is set to :input' do
         expect(repository).to receive(:update_autocrlf_option)
 
-        repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+        repository.update_branch_with_hooks(user, 'feature') { new_rev }
       end
 
       context "when the branch wasn't empty" do
         it 'updates the head' do
-          expect(repository.find_branch('feature').target.id).to eq(old_rev)
-          repository.commit_with_hooks(user, 'feature') { sample_commit.id }
-          expect(repository.find_branch('feature').target.id).to eq(sample_commit.id)
+          expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
+          repository.update_branch_with_hooks(user, 'feature') { new_rev }
+          expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
         end
       end
     end
 
+    context 'when the update adds more than one commit' do
+      it 'runs without errors' do
+        old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+
+        # old_rev is an ancestor of new_rev
+        expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
+
+        # old_rev is not a direct ancestor (parent) of new_rev
+        expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
+
+        branch = 'feature-ff-target'
+        repository.add_branch(user, branch, old_rev)
+
+        expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+      end
+    end
+
+    context 'when the update would remove commits from the target branch' do
+      it 'raises an exception' do
+        branch = 'master'
+        old_rev = repository.find_branch(branch).dereferenced_target.sha
+
+        # The 'master' branch is NOT an ancestor of new_rev.
+        expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
+
+        # Updating 'master' to new_rev would lose the commits on 'master' that
+        # are not contained in new_rev. This should not be allowed.
+        expect do
+          repository.update_branch_with_hooks(user, branch) { new_rev }
+        end.to raise_error(Repository::CommitError)
+      end
+    end
+
     context 'when pre hooks failed' do
       it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
-          repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+          repository.update_branch_with_hooks(user, 'feature') { new_rev }
         end.to raise_error(GitHooksService::PreReceiveError)
       end
     end
@@ -467,6 +747,7 @@ describe Repository, models: true do
     context 'when target branch is different from source branch' do
       before do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+        allow(repository).to receive(:update_ref!)
       end
 
       it 'expires branch cache' do
@@ -477,7 +758,7 @@ describe Repository, models: true do
         expect(repository).to     receive(:expire_has_visible_content_cache)
         expect(repository).to     receive(:expire_branch_count_cache)
 
-        repository.commit_with_hooks(user, 'new-feature') { sample_commit.id }
+        repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
       end
     end
 
@@ -719,6 +1000,14 @@ describe Repository, models: true do
       expect(merge_commit).to be_present
       expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
     end
+
+    it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+      merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+      merge_commit_id = repository.merge(user, merge_request, commit_options)
+      repository.commit(merge_commit_id)
+
+      expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+    end
   end
 
   describe '#revert' do
@@ -783,10 +1072,10 @@ describe Repository, models: true do
 
     context 'cherry-picking a merge commit' do
       it 'cherry-picks the changes' do
-        expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
+        expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
 
-        repository.cherry_pick(user, pickable_merge, 'master')
-        expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil
+        repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+        expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
       end
     end
   end
@@ -919,28 +1208,17 @@ describe Repository, models: true do
   end
 
   describe '#before_import' do
-    it 'flushes the emptiness cachess' do
-      expect(repository).to receive(:expire_emptiness_caches)
-
-      repository.before_import
-    end
-
-    it 'flushes the exists cache' do
-      expect(repository).to receive(:expire_exists_cache)
+    it 'flushes the repository caches' do
+      expect(repository).to receive(:expire_content_cache)
 
       repository.before_import
     end
   end
 
   describe '#after_import' do
-    it 'flushes the emptiness cachess' do
-      expect(repository).to receive(:expire_emptiness_caches)
-
-      repository.after_import
-    end
-
-    it 'flushes the exists cache' do
-      expect(repository).to receive(:expire_exists_cache)
+    it 'flushes and builds the cache' do
+      expect(repository).to receive(:expire_content_cache)
+      expect(repository).to receive(:build_cache)
 
       repository.after_import
     end
@@ -1242,4 +1520,28 @@ describe Repository, models: true do
       File.delete(path)
     end
   end
+
+  describe '#update_ref!' do
+    it 'can create a ref' do
+      repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+
+      expect(repository.find_branch('foobar')).not_to be_nil
+    end
+
+    it 'raises CommitError when the ref update fails' do
+      expect do
+        repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+      end.to raise_error(Repository::CommitError)
+    end
+  end
+
+  describe '#remove_storage_from_path' do
+    let(:storage_path) { project.repository_storage_path }
+    let(:project_path) { project.path_with_namespace }
+    let(:full_path) { File.join(storage_path, project_path) }
+
+    it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
+    it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
+    it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
+  end
 end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 05056a4bb4759db08fe49ca70605b43ba2c8a1be..43937a54b2cf53225d8c925789403a77ad247146 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -203,6 +203,23 @@ describe Service, models: true do
     end
   end
 
+  describe 'initialize service with no properties' do
+    let(:service) do
+      GitlabIssueTrackerService.create(
+        project: create(:project),
+        title: 'random title'
+      )
+    end
+
+    it 'does not raise error' do
+      expect { service }.not_to raise_error
+    end
+
+    it 'creates the properties' do
+      expect(service.properties).to eq({ "title" => "random title" })
+    end
+  end
+
   describe "callbacks" do
     let(:project) { create(:project) }
     let!(:service) do
@@ -221,7 +238,7 @@ describe Service, models: true do
       it "updates the has_external_issue_tracker boolean" do
         expect do
           service.save!
-        end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+        end.to change { service.project.has_external_issue_tracker }.from(false).to(true)
       end
     end
 
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 0621c6a06ce68adef67afdbca84c9643992fe8cd..f62f6bacbaa8fc82dacb663f32072b8dc06decbc 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -9,12 +9,14 @@ describe Snippet, models: true do
     it { is_expected.to include_module(Participable) }
     it { is_expected.to include_module(Referable) }
     it { is_expected.to include_module(Sortable) }
+    it { is_expected.to include_module(Awardable) }
   end
 
   describe 'associations' do
     it { is_expected.to belong_to(:author).class_name('User') }
     it { is_expected.to belong_to(:project) }
     it { is_expected.to have_many(:notes).dependent(:destroy) }
+    it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
   end
 
   describe 'validation' do
@@ -44,6 +46,13 @@ describe Snippet, models: true do
     end
   end
 
+  describe "#content_html_invalidated?" do
+    let(:snippet) { create(:snippet, content: "md", content_html: "html", file_name: "foo.md") }
+    it "invalidates the HTML cache of content when the filename changes" do
+      expect { snippet.file_name = "foo.rb" }.to change { snippet.content_html_invalidated? }.from(false).to(true)
+    end
+  end
+
   describe '.search' do
     let(:snippet) { create(:snippet) }
 
diff --git a/spec/models/trending_project_spec.rb b/spec/models/trending_project_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc28c6d4004791dd48713a13eac2d521b2ff9f46
--- /dev/null
+++ b/spec/models/trending_project_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe TrendingProject do
+  let(:user) { create(:user) }
+  let(:public_project1) { create(:empty_project, :public) }
+  let(:public_project2) { create(:empty_project, :public) }
+  let(:public_project3) { create(:empty_project, :public) }
+  let(:private_project) { create(:empty_project, :private) }
+  let(:internal_project) { create(:empty_project, :internal) }
+
+  before do
+    3.times do
+      create(:note_on_commit, project: public_project1)
+    end
+
+    2.times do
+      create(:note_on_commit, project: public_project2)
+    end
+
+    create(:note_on_commit, project: public_project3, created_at: 5.weeks.ago)
+    create(:note_on_commit, project: private_project)
+    create(:note_on_commit, project: internal_project)
+  end
+
+  describe '.refresh!' do
+    before do
+      described_class.refresh!
+    end
+
+    it 'populates the trending projects table' do
+      expect(described_class.count).to eq(2)
+    end
+
+    it 'removes existing rows before populating the table' do
+      described_class.refresh!
+
+      expect(described_class.count).to eq(2)
+    end
+
+    it 'stores the project IDs for every trending project' do
+      rows = described_class.order(id: :asc).all
+
+      expect(rows[0].project_id).to eq(public_project1.id)
+      expect(rows[1].project_id).to eq(public_project2.id)
+    end
+
+    it 'does not store projects that fall out of the trending time range' do
+      expect(described_class.where(project_id: public_project3).any?).to eq(false)
+    end
+
+    it 'stores only public projects' do
+      expect(described_class.where(project_id: [public_project1.id, public_project2.id]).count).to eq(2)
+      expect(described_class.where(project_id: [private_project.id, internal_project.id]).count).to eq(0)
+    end
+  end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 51e4780e2b1035b1bd96fee6ddb941a178885bf5..3b152e15b618ed7b5a8e4402095f7409a631cfda 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -15,11 +15,11 @@ describe User, models: true do
 
   describe 'associations' do
     it { is_expected.to have_one(:namespace) }
-    it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) }
+    it { is_expected.to have_many(:snippets).dependent(:destroy) }
     it { is_expected.to have_many(:project_members).dependent(:destroy) }
     it { is_expected.to have_many(:groups) }
     it { is_expected.to have_many(:keys).dependent(:destroy) }
-    it { is_expected.to have_many(:events).class_name('Event').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(:notes).dependent(:destroy) }
@@ -256,6 +256,20 @@ describe User, models: true do
         expect(users_without_two_factor).not_to include(user_with_2fa.id)
       end
     end
+
+    describe '.todo_authors' do
+      it 'filters users' do
+        create :user
+        user_2 = create :user
+        user_3 = create :user
+        current_user = create :user
+        create(:todo, user: current_user, author: user_2, state: :done)
+        create(:todo, user: current_user, author: user_3, state: :pending)
+
+        expect(User.todo_authors(current_user.id, 'pending')).to eq [user_3]
+        expect(User.todo_authors(current_user.id, 'done')).to eq [user_2]
+      end
+    end
   end
 
   describe "Respond to" do
@@ -599,6 +613,80 @@ describe User, models: true do
     end
   end
 
+  describe '.search_with_secondary_emails' do
+    def search_with_secondary_emails(query)
+      described_class.search_with_secondary_emails(query)
+    end
+
+    let!(:user) { create(:user) }
+    let!(:email) { create(:email) }
+
+    it 'returns users with a matching name' do
+      expect(search_with_secondary_emails(user.name)).to eq([user])
+    end
+
+    it 'returns users with a partially matching name' do
+      expect(search_with_secondary_emails(user.name[0..2])).to eq([user])
+    end
+
+    it 'returns users with a matching name regardless of the casing' do
+      expect(search_with_secondary_emails(user.name.upcase)).to eq([user])
+    end
+
+    it 'returns users with a matching email' do
+      expect(search_with_secondary_emails(user.email)).to eq([user])
+    end
+
+    it 'returns users with a partially matching email' do
+      expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+    end
+
+    it 'returns users with a matching email regardless of the casing' do
+      expect(search_with_secondary_emails(user.email.upcase)).to eq([user])
+    end
+
+    it 'returns users with a matching username' do
+      expect(search_with_secondary_emails(user.username)).to eq([user])
+    end
+
+    it 'returns users with a partially matching username' do
+      expect(search_with_secondary_emails(user.username[0..2])).to eq([user])
+    end
+
+    it 'returns users with a matching username regardless of the casing' do
+      expect(search_with_secondary_emails(user.username.upcase)).to eq([user])
+    end
+
+    it 'returns users with a matching whole secondary email' do
+      expect(search_with_secondary_emails(email.email)).to eq([email.user])
+    end
+
+    it 'returns users with a matching part of secondary email' do
+      expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
+    end
+
+    it 'return users with a matching part of secondary email regardless of case' do
+      expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
+      expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
+      expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
+    end
+
+    it 'returns multiple users with matching secondary emails' do
+      email1 = create(:email, email: '1_testemail@example.com')
+      email2 = create(:email, email: '2_testemail@example.com')
+      email3 = create(:email, email: 'other@email.com')
+      email3.user.update_attributes!(email: 'another@mail.com')
+
+      expect(
+        search_with_secondary_emails('testemail@example.com').map(&:id)
+      ).to include(email1.user.id, email2.user.id)
+
+      expect(
+        search_with_secondary_emails('testemail@example.com').map(&:id)
+      ).not_to include(email3.user.id)
+    end
+  end
+
   describe 'by_username_or_id' do
     let(:user1) { create(:user, username: 'foo') }
 
@@ -610,6 +698,23 @@ describe User, models: true do
     end
   end
 
+  describe '.find_by_ssh_key_id' do
+    context 'using an existing SSH key ID' do
+      let(:user) { create(:user) }
+      let(:key) { create(:key, user: user) }
+
+      it 'returns the corresponding User' do
+        expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
+      end
+    end
+
+    context 'using an invalid SSH key ID' do
+      it 'returns nil' do
+        expect(described_class.find_by_ssh_key_id(-1)).to be_nil
+      end
+    end
+  end
+
   describe '.by_login' do
     let(:username) { 'John' }
     let!(:user) { create(:user, username: username) }
@@ -920,6 +1025,16 @@ describe User, models: true do
 
       expect(subject.recent_push).to eq(nil)
     end
+
+    it "includes push events on any of the provided projects" do
+      expect(subject.recent_push(project1)).to eq(nil)
+      expect(subject.recent_push(project2)).to eq(push_event)
+
+      push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
+      push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+
+      expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
+    end
   end
 
   describe '#authorized_groups' do
@@ -996,8 +1111,7 @@ describe User, models: true do
     end
 
     it 'does not include projects for which issues are disabled' do
-      project = create(:project)
-      project.update_attributes(issues_enabled: false)
+      project = create(:project, issues_access_level: ProjectFeature::DISABLED)
 
       expect(user.projects_where_can_admin_issues.to_a).to be_empty
       expect(user.can?(:admin_issue, project)).to eq(false)
@@ -1091,4 +1205,40 @@ describe User, models: true do
       expect(user.viewable_starred_projects).not_to include(private_project)
     end
   end
+
+  describe '#projects_with_reporter_access_limited_to' do
+    let(:project1) { create(:project) }
+    let(:project2) { create(:project) }
+    let(:user) { create(:user) }
+
+    before do
+      project1.team << [user, :reporter]
+      project2.team << [user, :guest]
+    end
+
+    it 'returns the projects when using a single project ID' do
+      projects = user.projects_with_reporter_access_limited_to(project1.id)
+
+      expect(projects).to eq([project1])
+    end
+
+    it 'returns the projects when using an Array of project IDs' do
+      projects = user.projects_with_reporter_access_limited_to([project1.id])
+
+      expect(projects).to eq([project1])
+    end
+
+    it 'returns the projects when using an ActiveRecord relation' do
+      projects = user.
+        projects_with_reporter_access_limited_to(Project.select(:id))
+
+      expect(projects).to eq([project1])
+    end
+
+    it 'does not return projects you do not have reporter access to' do
+      projects = user.projects_with_reporter_access_limited_to(project2.id)
+
+      expect(projects).to be_empty
+    end
+  end
 end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7591bfd147145e3684a41f439000e95fe7c0aaeb
--- /dev/null
+++ b/spec/policies/issue_policy_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe IssuePolicy, models: true do
+  let(:user) { create(:user) }
+
+  describe '#rules' do
+    context 'using a regular issue' do
+      let(:project) { create(:project, :public) }
+      let(:issue) { create(:issue, project: project) }
+      let(:policies) { described_class.abilities(user, issue).to_set }
+
+      context 'with a regular user' do
+        it 'includes the read_issue permission' do
+          expect(policies).to include(:read_issue)
+        end
+
+        it 'does not include the admin_issue permission' do
+          expect(policies).not_to include(:admin_issue)
+        end
+
+        it 'does not include the update_issue permission' do
+          expect(policies).not_to include(:update_issue)
+        end
+      end
+
+      context 'with a user that is a project reporter' do
+        before do
+          project.team << [user, :reporter]
+        end
+
+        it 'includes the read_issue permission' do
+          expect(policies).to include(:read_issue)
+        end
+
+        it 'includes the admin_issue permission' do
+          expect(policies).to include(:admin_issue)
+        end
+
+        it 'includes the update_issue permission' do
+          expect(policies).to include(:update_issue)
+        end
+      end
+
+      context 'with a user that is a project guest' do
+        before do
+          project.team << [user, :guest]
+        end
+
+        it 'includes the read_issue permission' do
+          expect(policies).to include(:read_issue)
+        end
+
+        it 'does not include the admin_issue permission' do
+          expect(policies).not_to include(:admin_issue)
+        end
+
+        it 'does not include the update_issue permission' do
+          expect(policies).not_to include(:update_issue)
+        end
+      end
+    end
+
+    context 'using a confidential issue' do
+      let(:issue) { create(:issue, :confidential) }
+
+      context 'with a regular user' do
+        let(:policies) { described_class.abilities(user, issue).to_set }
+
+        it 'does not include the read_issue permission' do
+          expect(policies).not_to include(:read_issue)
+        end
+
+        it 'does not include the admin_issue permission' do
+          expect(policies).not_to include(:admin_issue)
+        end
+
+        it 'does not include the update_issue permission' do
+          expect(policies).not_to include(:update_issue)
+        end
+      end
+
+      context 'with a user that is a project member' do
+        let(:policies) { described_class.abilities(user, issue).to_set }
+
+        before do
+          issue.project.team << [user, :reporter]
+        end
+
+        it 'includes the read_issue permission' do
+          expect(policies).to include(:read_issue)
+        end
+
+        it 'includes the admin_issue permission' do
+          expect(policies).to include(:admin_issue)
+        end
+
+        it 'includes the update_issue permission' do
+          expect(policies).to include(:update_issue)
+        end
+      end
+
+      context 'without a user' do
+        let(:policies) { described_class.abilities(nil, issue).to_set }
+
+        it 'does not include the read_issue permission' do
+          expect(policies).not_to include(:read_issue)
+        end
+
+        it 'does not include the admin_issue permission' do
+          expect(policies).not_to include(:admin_issue)
+        end
+
+        it 'does not include the update_issue permission' do
+          expect(policies).not_to include(:update_issue)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b7b6cad6547c70d0cdef934679debfc9470de72
--- /dev/null
+++ b/spec/policies/issues_policy_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe IssuePolicy, models: true do
+  let(:guest) { create(:user) }
+  let(:author) { create(:user) }
+  let(:assignee) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:group) { create(:group, :public) }
+  let(:reporter_from_group_link) { create(:user) }
+
+  def permissions(user, issue)
+    IssuePolicy.abilities(user, issue).to_set
+  end
+
+  context 'a private project' do
+    let(:non_member) { create(:user) }
+    let(:project) { create(:empty_project, :private) }
+    let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+    let(:issue_no_assignee) { create(:issue, project: project) }
+
+    before do
+      project.team << [guest, :guest]
+      project.team << [author, :guest]
+      project.team << [assignee, :guest]
+      project.team << [reporter, :reporter]
+
+      group.add_reporter(reporter_from_group_link)
+
+      create(:project_group_link, group: group, project: project)
+    end
+
+    it 'does not allow non-members to read issues' do
+      expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+      expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+    end
+
+    it 'allows guests to read issues' do
+      expect(permissions(guest, issue)).to include(:read_issue)
+      expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+      expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    it 'allows reporters to read, update, and admin issues' do
+      expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+      expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+    end
+
+    it 'allows reporters from group links to read, update, and admin issues' do
+      expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+      expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+    end
+
+    it 'allows issue authors to read and update their issues' do
+      expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+      expect(permissions(author, issue)).not_to include(:admin_issue)
+
+      expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    it 'allows issue assignees to read and update their issues' do
+      expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+      expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+      expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    context 'with confidential issues' do
+      let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+      let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+      it 'does not allow non-members to read confidential issues' do
+        expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'does not allow guests to read confidential issues' do
+        expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows reporters to read, update, and admin confidential issues' do
+        expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows reporters from group links to read, update, and admin confidential issues' do
+        expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows issue authors to read and update their confidential issues' do
+        expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+        expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+        expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows issue assignees to read and update their confidential issues' do
+        expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+        expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+        expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+    end
+  end
+
+  context 'a public project' do
+    let(:project) { create(:empty_project, :public) }
+    let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+    let(:issue_no_assignee) { create(:issue, project: project) }
+
+    before do
+      project.team << [guest, :guest]
+      project.team << [reporter, :reporter]
+
+      group.add_reporter(reporter_from_group_link)
+
+      create(:project_group_link, group: group, project: project)
+    end
+
+    it 'allows guests to read issues' do
+      expect(permissions(guest, issue)).to include(:read_issue)
+      expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+      expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    it 'allows reporters to read, update, and admin issues' do
+      expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+      expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+    end
+
+    it 'allows reporters from group links to read, update, and admin issues' do
+      expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+      expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+    end
+
+    it 'allows issue authors to read and update their issues' do
+      expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+      expect(permissions(author, issue)).not_to include(:admin_issue)
+
+      expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    it 'allows issue assignees to read and update their issues' do
+      expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+      expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+      expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+      expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+    end
+
+    context 'with confidential issues' do
+      let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+      let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+      it 'does not allow guests to read confidential issues' do
+        expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows reporters to read, update, and admin confidential issues' do
+        expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows reporter from group links to read, update, and admin confidential issues' do
+        expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+        expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows issue authors to read and update their confidential issues' do
+        expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+        expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+        expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+
+      it 'allows issue assignees to read and update their confidential issues' do
+        expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+        expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+        expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+      end
+    end
+  end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96249a7d8c352a7ef32d6644a6439ea6736855ad
--- /dev/null
+++ b/spec/policies/project_policy_spec.rb
@@ -0,0 +1,179 @@
+require 'spec_helper'
+
+describe ProjectPolicy, models: true do
+  let(:guest) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:dev) { create(:user) }
+  let(:master) { create(:user) }
+  let(:owner) { create(:user) }
+  let(:admin) { create(:admin) }
+  let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
+
+  let(:guest_permissions) do
+    [
+      :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
+      :read_milestone, :read_project_snippet, :read_project_member,
+      :read_note, :create_project, :create_issue, :create_note,
+      :upload_file
+    ]
+  end
+
+  let(:reporter_permissions) do
+    [
+      :download_code, :fork_project, :create_project_snippet, :update_issue,
+      :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
+      :read_container_image, :read_pipeline, :read_environment, :read_deployment,
+      :read_merge_request
+    ]
+  end
+
+  let(:team_member_reporter_permissions) do
+    [
+      :build_download_code, :build_read_container_image
+    ]
+  end
+
+  let(:developer_permissions) do
+    [
+      :admin_merge_request, :update_merge_request, :create_commit_status,
+      :update_commit_status, :create_build, :update_build, :create_pipeline,
+      :update_pipeline, :create_merge_request, :create_wiki, :push_code,
+      :resolve_note, :create_container_image, :update_container_image,
+      :create_environment, :create_deployment
+    ]
+  end
+
+  let(:master_permissions) do
+    [
+      :push_code_to_protected_branches, :update_project_snippet, :update_environment,
+      :update_deployment, :admin_milestone, :admin_project_snippet,
+      :admin_project_member, :admin_note, :admin_wiki, :admin_project,
+      :admin_commit_status, :admin_build, :admin_container_image,
+      :admin_pipeline, :admin_environment, :admin_deployment
+    ]
+  end
+
+  let(:public_permissions) do
+    [
+      :download_code, :fork_project, :read_commit_status, :read_pipeline,
+      :read_container_image, :build_download_code, :build_read_container_image
+    ]
+  end
+
+  let(:owner_permissions) do
+    [
+      :change_namespace, :change_visibility_level, :rename_project, :remove_project,
+      :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+    ]
+  end
+
+  before do
+    project.team << [guest, :guest]
+    project.team << [master, :master]
+    project.team << [dev, :developer]
+    project.team << [reporter, :reporter]
+  end
+
+  it 'does not include the read_issue permission when the issue author is not a member of the private project' do
+    project = create(:project, :private)
+    issue   = create(:issue, project: project)
+    user    = issue.author
+
+    expect(project.team.member?(issue.author)).to eq(false)
+
+    expect(BasePolicy.class_for(project).abilities(user, project).can_set).
+      not_to include(:read_issue)
+
+    expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
+  end
+
+  context 'abilities for non-public projects' do
+    let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+    subject { described_class.abilities(current_user, project).to_set }
+
+    context 'with no user' do
+      let(:current_user) { nil }
+
+      it { is_expected.to be_empty }
+    end
+
+    context 'guests' do
+      let(:current_user) { guest }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.not_to include(*reporter_permissions)
+        is_expected.not_to include(*team_member_reporter_permissions)
+        is_expected.not_to include(*developer_permissions)
+        is_expected.not_to include(*master_permissions)
+        is_expected.not_to include(*owner_permissions)
+      end
+    end
+
+    context 'reporter' do
+      let(:current_user) { reporter }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.to include(*reporter_permissions)
+        is_expected.to include(*team_member_reporter_permissions)
+        is_expected.not_to include(*developer_permissions)
+        is_expected.not_to include(*master_permissions)
+        is_expected.not_to include(*owner_permissions)
+      end
+    end
+
+    context 'developer' do
+      let(:current_user) { dev }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.to include(*reporter_permissions)
+        is_expected.to include(*team_member_reporter_permissions)
+        is_expected.to include(*developer_permissions)
+        is_expected.not_to include(*master_permissions)
+        is_expected.not_to include(*owner_permissions)
+      end
+    end
+
+    context 'master' do
+      let(:current_user) { master }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.to include(*reporter_permissions)
+        is_expected.to include(*team_member_reporter_permissions)
+        is_expected.to include(*developer_permissions)
+        is_expected.to include(*master_permissions)
+        is_expected.not_to include(*owner_permissions)
+      end
+    end
+
+    context 'owner' do
+      let(:current_user) { owner }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.to include(*reporter_permissions)
+        is_expected.to include(*team_member_reporter_permissions)
+        is_expected.to include(*developer_permissions)
+        is_expected.to include(*master_permissions)
+        is_expected.to include(*owner_permissions)
+      end
+    end
+
+    context 'admin' do
+      let(:current_user) { admin }
+
+      it do
+        is_expected.to include(*guest_permissions)
+        is_expected.to include(*reporter_permissions)
+        is_expected.not_to include(*team_member_reporter_permissions)
+        is_expected.to include(*developer_permissions)
+        is_expected.to include(*master_permissions)
+        is_expected.to include(*owner_permissions)
+      end
+    end
+  end
+end
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b5b4bf9feaceca152c63183f92a9583983b6031
--- /dev/null
+++ b/spec/rake_helper.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'rake'
+
+RSpec.configure do |config|
+  config.include RakeHelpers
+
+  # Redirect stdout so specs don't have so much noise
+  config.before(:all) do
+    $stdout = StringIO.new
+
+    Rake.application.rake_require 'tasks/gitlab/task_helpers'
+    Rake::Task.define_task :environment
+  end
+
+  # Reset stdout
+  config.after(:all) do
+    $stdout = STDOUT
+  end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index d78494b76fac2a281852498c867b575a40a2454e..b467890a4030c77314ef39348a1f3880f8ff7eaa 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -64,12 +64,12 @@ describe API::AccessRequests, api: true  do
       context 'when authenticated as a member' do
         %i[developer master].each do |type|
           context "as a #{type}" do
-            it 'returns 400' do
+            it 'returns 403' do
               expect do
                 user = public_send(type)
                 post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
 
-                expect(response).to have_http_status(400)
+                expect(response).to have_http_status(403)
               end.not_to change { source.requesters.count }
             end
           end
@@ -87,6 +87,20 @@ describe API::AccessRequests, api: true  do
       end
 
       context 'when authenticated as a stranger' do
+        context "when access request is disabled for the #{source_type}" do
+          before do
+            source.update(request_access_enabled: false)
+          end
+
+          it 'returns 403' do
+            expect do
+              post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+              expect(response).to have_http_status(403)
+            end.not_to change { source.requesters.count }
+          end
+        end
+
         it 'returns 201' do
           expect do
             post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
@@ -181,7 +195,7 @@ describe API::AccessRequests, api: true  do
       end
 
       context 'when authenticated as the access requester' do
-        it 'returns 200' do
+        it 'deletes the access requester' do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
 
@@ -191,7 +205,7 @@ describe API::AccessRequests, api: true  do
       end
 
       context 'when authenticated as a master/owner' do
-        it 'returns 200' do
+        it 'deletes the access requester' do
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
 
@@ -199,6 +213,16 @@ describe API::AccessRequests, api: true  do
           end.to change { source.requesters.count }.by(-1)
         end
 
+        context 'user_id matches a member, not an access requester' do
+          it 'returns 404' do
+            expect do
+              delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.requesters.count }
+          end
+        end
+
         context 'user_id does not match an existing access requester' do
           it 'returns 404' do
             expect do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index c65510fadecf0dc1aafabb76ac52ce064c58f4f7..01bb9e955e01c4201e4fe75b2524cda398076f9b 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -3,13 +3,15 @@ require 'spec_helper'
 describe API::Helpers, api: true do
   include API::Helpers
   include ApiHelpers
+  include SentryHelper
 
   let(:user) { create(:user) }
   let(:admin) { create(:admin) }
   let(:key) { create(:key, user: user) }
 
   let(:params) { {} }
-  let(:env) { {} }
+  let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+  let(:request) { Rack::Request.new(env) }
 
   def set_env(token_usr, identifier)
     clear_env
@@ -35,11 +37,62 @@ describe API::Helpers, api: true do
     params.delete(API::Helpers::SUDO_PARAM)
   end
 
+  def warden_authenticate_returns(value)
+    warden = double("warden", authenticate: value)
+    env['warden'] = warden
+  end
+
+  def doorkeeper_guard_returns(value)
+    allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
+  end
+
   def error!(message, status)
     raise Exception
   end
 
   describe ".current_user" do
+    subject { current_user }
+
+    describe "Warden authentication" do
+      before { doorkeeper_guard_returns false }
+
+      context "with invalid credentials" do
+        context "GET request" do
+          before { env['REQUEST_METHOD'] = 'GET' }
+          it { is_expected.to be_nil }
+        end
+      end
+
+      context "with valid credentials" do
+        before { warden_authenticate_returns user }
+
+        context "GET request" do
+          before { env['REQUEST_METHOD'] = 'GET' }
+          it { is_expected.to eq(user) }
+        end
+
+        context "HEAD request" do
+          before { env['REQUEST_METHOD'] = 'HEAD' }
+          it { is_expected.to eq(user) }
+        end
+
+        context "PUT request" do
+          before { env['REQUEST_METHOD'] = 'PUT' }
+          it { is_expected.to be_nil }
+        end
+
+        context "POST request" do
+          before { env['REQUEST_METHOD'] = 'POST' }
+          it { is_expected.to be_nil }
+        end
+
+        context "DELETE request" do
+          before { env['REQUEST_METHOD'] = 'DELETE' }
+          it { is_expected.to be_nil }
+        end
+      end
+    end
+
     describe "when authenticating using a user's private token" do
       it "returns nil for an invalid token" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
@@ -212,26 +265,29 @@ describe API::Helpers, api: true do
     end
   end
 
-  describe '.to_boolean' do
-    it 'converts a valid string to a boolean' do
-      expect(to_boolean('true')).to be_truthy
-      expect(to_boolean('YeS')).to be_truthy
-      expect(to_boolean('t')).to be_truthy
-      expect(to_boolean('1')).to be_truthy
-      expect(to_boolean('ON')).to be_truthy
-      expect(to_boolean('FaLse')).to be_falsy
-      expect(to_boolean('F')).to be_falsy
-      expect(to_boolean('NO')).to be_falsy
-      expect(to_boolean('n')).to be_falsy
-      expect(to_boolean('0')).to be_falsy
-      expect(to_boolean('oFF')).to be_falsy
+  describe '.handle_api_exception' do
+    before do
+      allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
+      allow_any_instance_of(self.class).to receive(:rack_response)
+    end
+
+    it 'does not report a MethodNotAllowed exception to Sentry' do
+      exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
+      allow(exception).to receive(:backtrace).and_return(caller)
+
+      expect(Raven).not_to receive(:capture_exception).with(exception)
+
+      handle_api_exception(exception)
     end
 
-    it 'converts an invalid string to nil' do
-      expect(to_boolean('fals')).to be_nil
-      expect(to_boolean('yeah')).to be_nil
-      expect(to_boolean('')).to be_nil
-      expect(to_boolean(nil)).to be_nil
+    it 'does report RuntimeError to Sentry' do
+      exception = RuntimeError.new('test error')
+      allow(exception).to receive(:backtrace).and_return(caller)
+
+      expect_any_instance_of(self.class).to receive(:sentry_context)
+      expect(Raven).to receive(:capture_exception).with(exception)
+
+      handle_api_exception(exception)
     end
   end
 end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 73c268c0d1ef482835ee9f08f1d7cdfb98a58e72..5ad4fc4865aebf7cd14cbb29c6329011e50d7aa4 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
 describe API::API, api: true  do
   include ApiHelpers
   let(:user)            { create(:user) }
-  let!(:project)        { create(:project) }
-  let(:issue)           { create(:issue, project: project, author: 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) }
@@ -39,6 +39,19 @@ describe API::API, api: true  do
       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 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)
@@ -91,6 +104,20 @@ describe API::API, api: true  do
       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 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)
@@ -115,6 +142,8 @@ describe API::API, api: true  do
   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 api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
@@ -136,6 +165,12 @@ describe API::API, api: true  do
         expect(response).to have_http_status(401)
       end
 
+      it "returns a 404 error if the user authored issue" do
+        post 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 api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
 
@@ -152,9 +187,23 @@ describe API::API, api: true  do
         end
       end
     end
+
+    context 'on a snippet' do
+      it 'creates a new award emoji' do
+        snippet = create(:project_snippet, :public, project: project)
+
+        post 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 api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
@@ -164,6 +213,12 @@ describe API::API, api: true  do
       expect(json_response['user']['username']).to eq(user.username)
     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'
+
+      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'
 
@@ -213,6 +268,19 @@ describe API::API, api: true  do
         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 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)
+      end
+    end
   end
 
   describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f5c09a30291a99a29fb8057b7855909194e24e4
--- /dev/null
+++ b/spec/requests/api/boards_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+
+describe API::API, api: true  do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:user2)       { create(:user) }
+  let(:non_member)  { create(:user) }
+  let(:guest)       { create(:user) }
+  let(:admin)       { create(:user, :admin) }
+  let!(:project)    { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+  let!(:dev_label) do
+    create(:label, title: 'Development', color: '#FFAABB', project: project)
+  end
+
+  let!(:test_label) do
+    create(:label, title: 'Testing', color: '#FFAACC', project: project)
+  end
+
+  let!(:ux_label) do
+    create(:label, title: 'UX', color: '#FF0000', project: project)
+  end
+
+  let!(:dev_list) do
+    create(:list, label: dev_label, position: 1)
+  end
+
+  let!(:test_list) do
+    create(:list, label: test_label, position: 2)
+  end
+
+  let!(:board) do
+    create(:board, project: project, lists: [dev_list, test_list])
+  end
+
+  before do
+    project.team << [user, :reporter]
+    project.team << [guest, :guest]
+  end
+
+  describe "GET /projects/:id/boards" do
+    let(:base_url) { "/projects/#{project.id}/boards" }
+
+    context "when unauthenticated" do
+      it "returns authentication error" do
+        get api(base_url)
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when authenticated" do
+      it "returns the project issue board" do
+        get api(base_url, 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(board.id)
+        expect(json_response.first['lists']).to be_an Array
+        expect(json_response.first['lists'].length).to eq(2)
+        expect(json_response.first['lists'].last).to have_key('position')
+      end
+    end
+  end
+
+  describe "GET /projects/:id/boards/:board_id/lists" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it 'returns issue board lists' do
+      get api(base_url, user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(2)
+      expect(json_response.first['label']['name']).to eq(dev_label.title)
+    end
+
+    it 'returns 404 if board not found' do
+      get api("/projects/#{project.id}/boards/22343/lists", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it 'returns a list' do
+      get api("#{base_url}/#{dev_list.id}", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['id']).to eq(dev_list.id)
+      expect(json_response['label']['name']).to eq(dev_label.title)
+      expect(json_response['position']).to eq(1)
+    end
+
+    it 'returns 404 if list not found' do
+      get api("#{base_url}/5324", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe "POST /projects/:id/board/lists" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it 'creates a new issue board list for group labels' do
+      group = create(:group)
+      group_label = create(:group_label, group: group)
+      project.update(group: group)
+
+      post api(base_url, user), label_id: group_label.id
+
+      expect(response).to have_http_status(201)
+      expect(json_response['label']['name']).to eq(group_label.title)
+      expect(json_response['position']).to eq(3)
+    end
+
+    it 'creates a new issue board list for project labels' do
+      post api(base_url, user), label_id: ux_label.id
+
+      expect(response).to have_http_status(201)
+      expect(json_response['label']['name']).to eq(ux_label.title)
+      expect(json_response['position']).to eq(3)
+    end
+
+    it 'returns 400 when creating a new list if label_id is invalid' do
+      post api(base_url, user), label_id: 23423
+
+      expect(response).to have_http_status(400)
+    end
+
+    it 'returns 403 for project members with guest role' do
+      put api("#{base_url}/#{test_list.id}", guest), position: 1
+
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it "updates a list" do
+      put api("#{base_url}/#{test_list.id}", user),
+        position: 1
+
+      expect(response).to have_http_status(200)
+      expect(json_response['position']).to eq(1)
+    end
+
+    it "returns 404 error if list id not found" do
+      put api("#{base_url}/44444", user),
+        position: 1
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 403 for project members with guest role" do
+      put api("#{base_url}/#{test_list.id}", guest),
+        position: 1
+
+      expect(response).to have_http_status(403)
+    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 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 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 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(:project, namespace: owner.namespace) }
+
+      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)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 3fd989dd7a612ecd2d9773b171f2f240ddf94805..1711096f4bd4a6101fe5ae229b6a4b29d56a92d7 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -48,92 +48,142 @@ describe API::API, api: true  do
   end
 
   describe 'PUT /projects/:id/repository/branches/:branch/protect' do
-    it 'protects a single branch' do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+    context "when a protected branch doesn't already exist" do
+      it 'protects a single branch' do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(branch_name)
-      expect(json_response['commit']['id']).to eq(branch_sha)
-      expect(json_response['protected']).to eq(true)
-      expect(json_response['developers_can_push']).to eq(false)
-      expect(json_response['developers_can_merge']).to eq(false)
-    end
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['commit']['id']).to eq(branch_sha)
+        expect(json_response['protected']).to eq(true)
+        expect(json_response['developers_can_push']).to eq(false)
+        expect(json_response['developers_can_merge']).to eq(false)
+      end
 
-    it 'protects a single branch and developers can push' do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
-        developers_can_push: true
+      it 'protects a single branch and developers can push' do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+            developers_can_push: true
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(branch_name)
-      expect(json_response['commit']['id']).to eq(branch_sha)
-      expect(json_response['protected']).to eq(true)
-      expect(json_response['developers_can_push']).to eq(true)
-      expect(json_response['developers_can_merge']).to eq(false)
-    end
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['commit']['id']).to eq(branch_sha)
+        expect(json_response['protected']).to eq(true)
+        expect(json_response['developers_can_push']).to eq(true)
+        expect(json_response['developers_can_merge']).to eq(false)
+      end
 
-    it 'protects a single branch and developers can merge' do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
-        developers_can_merge: true
+      it 'protects a single branch and developers can merge' do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+            developers_can_merge: true
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(branch_name)
-      expect(json_response['commit']['id']).to eq(branch_sha)
-      expect(json_response['protected']).to eq(true)
-      expect(json_response['developers_can_push']).to eq(false)
-      expect(json_response['developers_can_merge']).to eq(true)
-    end
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['commit']['id']).to eq(branch_sha)
+        expect(json_response['protected']).to eq(true)
+        expect(json_response['developers_can_push']).to eq(false)
+        expect(json_response['developers_can_merge']).to eq(true)
+      end
 
-    it 'protects a single branch and developers can push and merge' do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
-        developers_can_push: true, developers_can_merge: true
+      it 'protects a single branch and developers can push and merge' do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+            developers_can_push: true, developers_can_merge: true
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(branch_name)
-      expect(json_response['commit']['id']).to eq(branch_sha)
-      expect(json_response['protected']).to eq(true)
-      expect(json_response['developers_can_push']).to eq(true)
-      expect(json_response['developers_can_merge']).to eq(true)
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['commit']['id']).to eq(branch_sha)
+        expect(json_response['protected']).to eq(true)
+        expect(json_response['developers_can_push']).to eq(true)
+        expect(json_response['developers_can_merge']).to eq(true)
+      end
     end
 
-    it 'protects a single branch and developers cannot push and merge' do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
-        developers_can_push: 'tru', developers_can_merge: 'tr'
+    context 'for an existing protected branch' do
+      before do
+        project.repository.add_branch(user, protected_branch.name, 'master')
+      end
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(branch_name)
-      expect(json_response['commit']['id']).to eq(branch_sha)
-      expect(json_response['protected']).to eq(true)
-      expect(json_response['developers_can_push']).to eq(false)
-      expect(json_response['developers_can_merge']).to eq(false)
-    end
+      context "when developers can push and merge" do
+        let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') }
+
+        it 'updates that a developer cannot push or merge' do
+          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+              developers_can_push: false, developers_can_merge: false
+
+          expect(response).to have_http_status(200)
+          expect(json_response['name']).to eq(protected_branch.name)
+          expect(json_response['protected']).to eq(true)
+          expect(json_response['developers_can_push']).to eq(false)
+          expect(json_response['developers_can_merge']).to eq(false)
+        end
+
+        it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do
+          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+              developers_can_push: false
+
+          expect(response).to have_http_status(200)
+          expect(json_response['name']).to eq(protected_branch.name)
+          expect(protected_branch.reload.push_access_levels.first).to be_present
+          expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+        end
+
+        it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do
+          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+              developers_can_merge: false
+
+          expect(response).to have_http_status(200)
+          expect(json_response['name']).to eq(protected_branch.name)
+          expect(protected_branch.reload.merge_access_levels.first).to be_present
+          expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+        end
+      end
 
-    context 'on a protected branch' do
-      let(:protected_branch) { 'foo' }
+      context "when developers cannot push or merge" do
+        let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') }
 
-      before do
-        project.repository.add_branch(user, protected_branch, 'master')
-        create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch)
+        it 'updates that a developer can push and merge' do
+          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+              developers_can_push: true, developers_can_merge: true
+
+          expect(response).to have_http_status(200)
+          expect(json_response['name']).to eq(protected_branch.name)
+          expect(json_response['protected']).to eq(true)
+          expect(json_response['developers_can_push']).to eq(true)
+          expect(json_response['developers_can_merge']).to eq(true)
+        end
       end
+    end
 
-      it 'updates that a developer can push' do
-        put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
-          developers_can_push: false, developers_can_merge: false
+    context "multiple API calls" do
+      it "returns success when `protect` is called twice" do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq(protected_branch)
+        expect(json_response['name']).to eq(branch_name)
         expect(json_response['protected']).to eq(true)
         expect(json_response['developers_can_push']).to eq(false)
         expect(json_response['developers_can_merge']).to eq(false)
       end
 
-      it 'does not update that a developer can push' do
-        put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
-          developers_can_push: 'foobar', developers_can_merge: 'foo'
+      it "returns success when `protect` is called twice with `developers_can_push` turned on" do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq(protected_branch)
+        expect(json_response['name']).to eq(branch_name)
         expect(json_response['protected']).to eq(true)
         expect(json_response['developers_can_push']).to eq(true)
+        expect(json_response['developers_can_merge']).to eq(false)
+      end
+
+      it "returns success when `protect` is called twice with `developers_can_merge` turned on" do
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['protected']).to eq(true)
+        expect(json_response['developers_can_push']).to eq(false)
         expect(json_response['developers_can_merge']).to eq(true)
       end
     end
@@ -147,12 +197,6 @@ describe API::API, api: true  do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
       expect(response).to have_http_status(403)
     end
-
-    it "returns success when protect branch again" do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
-      expect(response).to have_http_status(200)
-    end
   end
 
   describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c9078b28642cd1d44b9675368ffc7152659c40a
--- /dev/null
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+
+describe API::BroadcastMessages, api: true do
+  include ApiHelpers
+
+  let(:user)  { create(:user) }
+  let(:admin) { create(:admin) }
+
+  describe 'GET /broadcast_messages' do
+    it 'returns a 401 for anonymous users' do
+      get api('/broadcast_messages')
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      get api('/broadcast_messages', user)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns an Array of BroadcastMessages for admins' do
+      create(:broadcast_message)
+
+      get api('/broadcast_messages', admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_kind_of(Array)
+      expect(json_response.first.keys)
+        .to match_array(%w(id message starts_at ends_at color font active))
+    end
+  end
+
+  describe 'GET /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      get api("/broadcast_messages/#{message.id}")
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      get api("/broadcast_messages/#{message.id}", user)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns the specified message for admins' do
+      get api("/broadcast_messages/#{message.id}", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['id']).to eq message.id
+      expect(json_response.keys)
+        .to match_array(%w(id message starts_at ends_at color font active))
+    end
+  end
+
+  describe 'POST /broadcast_messages' do
+    it 'returns a 401 for anonymous users' do
+      post api('/broadcast_messages'), attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      post api('/broadcast_messages', user), attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    context 'as an admin' do
+      it 'requires the `message` parameter' do
+        attrs = attributes_for(:broadcast_message)
+        attrs.delete(:message)
+
+        post api('/broadcast_messages', admin), attrs
+
+        expect(response).to have_http_status(400)
+        expect(json_response['error']).to eq 'message is missing'
+      end
+
+      it 'defines sane default start and end times' do
+        time = Time.zone.parse('2016-07-02 10:11:12')
+        travel_to(time) do
+          post api('/broadcast_messages', admin), message: 'Test message'
+
+          expect(response).to have_http_status(201)
+          expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+          expect(json_response['ends_at']).to   eq '2016-07-02T11:11:12.000Z'
+        end
+      end
+
+      it 'accepts a custom background and foreground color' do
+        attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece')
+
+        post api('/broadcast_messages', admin), attrs
+
+        expect(response).to have_http_status(201)
+        expect(json_response['color']).to eq attrs[:color]
+        expect(json_response['font']).to eq attrs[:font]
+      end
+    end
+  end
+
+  describe 'PUT /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      put api("/broadcast_messages/#{message.id}"),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      put api("/broadcast_messages/#{message.id}", user),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    context 'as an admin' do
+      it 'accepts new background and foreground colors' do
+        attrs = { color: '#000000', font: '#cecece' }
+
+        put api("/broadcast_messages/#{message.id}", admin), attrs
+
+        expect(response).to have_http_status(200)
+        expect(json_response['color']).to eq attrs[:color]
+        expect(json_response['font']).to eq attrs[:font]
+      end
+
+      it 'accepts new start and end times' do
+        time = Time.zone.parse('2016-07-02 10:11:12')
+        travel_to(time) do
+          attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now }
+
+          put api("/broadcast_messages/#{message.id}", admin), attrs
+
+          expect(response).to have_http_status(200)
+          expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+          expect(json_response['ends_at']).to   eq '2016-07-02T13:11:12.000Z'
+        end
+      end
+
+      it 'accepts a new message' do
+        attrs = { message: 'new message' }
+
+        put api("/broadcast_messages/#{message.id}", admin), attrs
+
+        expect(response).to have_http_status(200)
+        expect { message.reload }.to change { message.message }.to('new message')
+      end
+    end
+  end
+
+  describe 'DELETE /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      delete 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 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 { delete api("/broadcast_messages/#{message.id}", admin) }
+        .to change { BroadcastMessage.count }.by(-1)
+    end
+  end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 41503885dd94547e7a85b9409f165e344b287a22..fc72a44d663c1a99334f1940817cc3657efd6080 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -15,7 +15,9 @@ describe API::API, api: true do
   describe 'GET /projects/:id/builds ' do
     let(:query) { '' }
 
-    before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
+    before do
+      get api("/projects/#{project.id}/builds?#{query}", api_user)
+    end
 
     context 'authorized user' do
       it 'returns project builds' do
@@ -28,6 +30,15 @@ describe API::API, api: true do
         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' }
 
@@ -89,6 +100,15 @@ describe API::API, api: true do
             expect(json_response).to be_an Array
             expect(json_response.size).to eq 2
           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
         end
 
         context 'when pipeline has no builds' do
@@ -122,13 +142,24 @@ describe API::API, api: true do
   end
 
   describe 'GET /projects/:id/builds/:build_id' do
-    before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
+    before do
+      get api("/projects/#{project.id}/builds/#{build.id}", api_user)
+    end
 
     context 'authorized user' do
       it 'returns specific build 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
@@ -141,7 +172,9 @@ describe API::API, api: true do
   end
 
   describe 'GET /projects/:id/builds/:build_id/artifacts' do
-    before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
+    before do
+      get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+    end
 
     context 'build with artifacts' do
       let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
@@ -244,6 +277,7 @@ describe API::API, api: true do
 
       context 'with regular branch' do
         before do
+          pipeline.reload
           pipeline.update(ref: 'master',
                           sha: project.commit('master').sha)
 
@@ -255,6 +289,7 @@ describe API::API, api: true do
 
       context 'with branch name containing slash' do
         before do
+          pipeline.reload
           pipeline.update(ref: 'improve/awesome',
                           sha: project.commit('improve/awesome').sha)
         end
@@ -292,7 +327,9 @@ describe API::API, api: true do
   end
 
   describe 'POST /projects/:id/builds/:build_id/cancel' do
-    before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) }
+    before do
+      post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
+    end
 
     context 'authorized user' do
       context 'user with :update_build persmission' do
@@ -323,7 +360,9 @@ describe API::API, api: true do
   describe 'POST /projects/:id/builds/:build_id/retry' do
     let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
 
-    before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
+    before do
+      post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
+    end
 
     context 'authorized user' do
       context 'user with :update_build permission' do
@@ -407,4 +446,27 @@ describe API::API, api: true do
       end
     end
   end
+
+  describe 'POST /projects/:id/builds/:build_id/play' do
+    before do
+      post api("/projects/#{project.id}/builds/#{build.id}/play", user)
+    end
+
+    context 'on an playable build' do
+      let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+      it 'plays the build' 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 build' do
+      it 'returns a status code 400, Bad Request' do
+        expect(response).to have_http_status 400
+        expect(response.body).to match("Unplayable Build")
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 2d6093fec7a467eb9b7545729ef348cefae68e25..335efc4db6cf69dcbf3e6262d47419cd89b07cfa 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -117,17 +117,36 @@ describe API::CommitStatuses, api: true do
     let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
 
     context 'developer user' do
-      context 'only required parameters' do
-        before { post api(post_url, developer), state: 'success' }
+      %w[pending running success failed canceled].each do |status|
+        context "for #{status}" do
+          context 'uses only required parameters' do
+            it 'creates commit status' do
+              post api(post_url, developer), state: status
+
+              expect(response).to have_http_status(201)
+              expect(json_response['sha']).to eq(commit.id)
+              expect(json_response['status']).to eq(status)
+              expect(json_response['name']).to eq('default')
+              expect(json_response['ref']).not_to be_empty
+              expect(json_response['target_url']).to be_nil
+              expect(json_response['description']).to be_nil
+            end
+          end
+        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('default')
-          expect(json_response['ref']).to be_nil
-          expect(json_response['target_url']).to be_nil
-          expect(json_response['description']).to be_nil
+      context 'transitions status from pending' do
+        before do
+          post api(post_url, developer), state: 'pending'
+        end
+
+        %w[running success failed canceled].each do |status|
+          it "to #{status}" do
+            expect { post api(post_url, developer), state: status }.not_to change { CommitStatus.count }
+
+            expect(response).to have_http_status(201)
+            expect(json_response['status']).to eq(status)
+          end
         end
       end
 
@@ -177,7 +196,7 @@ describe API::CommitStatuses, api: true do
     end
 
     context 'reporter user' do
-      before { post api(post_url, reporter) }
+      before { post api(post_url, reporter), state: 'running' }
 
       it 'does not create commit status' do
         expect(response).to have_http_status(403)
@@ -185,7 +204,7 @@ describe API::CommitStatuses, api: true do
     end
 
     context 'guest user' do
-      before { post api(post_url, guest) }
+      before { post api(post_url, guest), state: 'running' }
 
       it 'does not create commit status' do
         expect(response).to have_http_status(403)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 7ca75d776733343d0988c00cb64f5c190c3042af..a6e8550fac39e27c1e732564f9aaced67430b492 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true  do
   include ApiHelpers
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
-  let!(:project) { create(:project, creator_id: user.id) }
+  let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
   let!(:master) { create(:project_member, :master, user: user, project: project) }
   let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
   let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
@@ -13,7 +13,7 @@ describe API::API, api: true  do
 
   before { project.team << [user, :reporter] }
 
-  describe "GET /projects/:id/repository/commits" do
+  describe "List repository commits" do
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
@@ -53,7 +53,12 @@ describe API::API, api: true  do
 
         get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
 
-        expect(json_response.size).to eq(commits.size - 1)
+        if commits.size >= 20
+          expect(json_response.size).to eq(20)
+        else
+          expect(json_response.size).to eq(commits.size - 1)
+        end
+
         expect(json_response.first["id"]).to eq(commits.second.id)
         expect(json_response.second["id"]).to eq(commits.third.id)
       end
@@ -67,9 +72,281 @@ describe API::API, api: true  do
         expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format"
       end
     end
+
+    context "path optional parameter" do
+      it "returns project commits matching provided path parameter" do
+        path = 'files/ruby/popen.rb'
+
+        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")
+      end
+    end
+  end
+
+  describe "Create a commit with multiple files and actions" do
+    let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+    it 'returns a 403 unauthorized for user without permissions' do
+      post api(url, user2)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns a 400 bad request if no params are given' do
+      post api(url, user)
+
+      expect(response).to have_http_status(400)
+    end
+
+    context :create do
+      let(:message) { 'Created file' }
+      let!(:invalid_c_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'create',
+              file_path: 'files/ruby/popen.rb',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+      let!(:valid_c_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'create',
+              file_path: 'foo/bar/baz.txt',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+
+      it 'a new file in project repo' do
+        post api(url, user), valid_c_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq(message)
+      end
+
+      it 'returns a 400 bad request if file exists' do
+        post api(url, user), invalid_c_params
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context :delete do
+      let(:message) { 'Deleted file' }
+      let!(:invalid_d_params) do
+        {
+          branch_name: 'markdown',
+          commit_message: message,
+          actions: [
+            {
+              action: 'delete',
+              file_path: 'doc/api/projects.md'
+            }
+          ]
+        }
+      end
+      let!(:valid_d_params) do
+        {
+          branch_name: 'markdown',
+          commit_message: message,
+          actions: [
+            {
+              action: 'delete',
+              file_path: 'doc/api/users.md'
+            }
+          ]
+        }
+      end
+
+      it 'an existing file in project repo' do
+        post api(url, user), valid_d_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq(message)
+      end
+
+      it 'returns a 400 bad request if file does not exist' do
+        post api(url, user), invalid_d_params
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context :move do
+      let(:message) { 'Moved file' }
+      let!(:invalid_m_params) do
+        {
+          branch_name: 'feature',
+          commit_message: message,
+          actions: [
+            {
+              action: 'move',
+              file_path: 'CHANGELOG',
+              previous_path: 'VERSION',
+              content: '6.7.0.pre'
+            }
+          ]
+        }
+      end
+      let!(:valid_m_params) do
+        {
+          branch_name: 'feature',
+          commit_message: message,
+          actions: [
+            {
+              action: 'move',
+              file_path: 'VERSION.txt',
+              previous_path: 'VERSION',
+              content: '6.7.0.pre'
+            }
+          ]
+        }
+      end
+
+      it 'an existing file in project repo' do
+        post api(url, user), valid_m_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq(message)
+      end
+
+      it 'returns a 400 bad request if file does not exist' do
+        post api(url, user), invalid_m_params
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context :update do
+      let(:message) { 'Updated file' }
+      let!(:invalid_u_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'update',
+              file_path: 'foo/bar.baz',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+      let!(:valid_u_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'update',
+              file_path: 'files/ruby/popen.rb',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+
+      it 'an existing file in project repo' do
+        post api(url, user), valid_u_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq(message)
+      end
+
+      it 'returns a 400 bad request if file does not exist' do
+        post api(url, user), invalid_u_params
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context "multiple operations" do
+      let(:message) { 'Multiple actions' }
+      let!(:invalid_mo_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'create',
+              file_path: 'files/ruby/popen.rb',
+              content: 'puts 8'
+            },
+            {
+              action: 'delete',
+              file_path: 'doc/api/projects.md'
+            },
+            {
+              action: 'move',
+              file_path: 'CHANGELOG',
+              previous_path: 'VERSION',
+              content: '6.7.0.pre'
+            },
+            {
+              action: 'update',
+              file_path: 'foo/bar.baz',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+      let!(:valid_mo_params) do
+        {
+          branch_name: 'master',
+          commit_message: message,
+          actions: [
+            {
+              action: 'create',
+              file_path: 'foo/bar/baz.txt',
+              content: 'puts 8'
+            },
+            {
+              action: 'delete',
+              file_path: 'Gemfile.zip'
+            },
+            {
+              action: 'move',
+              file_path: 'VERSION.txt',
+              previous_path: 'VERSION',
+              content: '6.7.0.pre'
+            },
+            {
+              action: 'update',
+              file_path: 'files/ruby/popen.rb',
+              content: 'puts 8'
+            }
+          ]
+        }
+      end
+
+      it 'are commited as one in project repo' do
+        post api(url, user), valid_mo_params
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq(message)
+      end
+
+      it 'return a 400 bad request if there are any issues' do
+        post api(url, user), invalid_mo_params
+
+        expect(response).to have_http_status(400)
+      end
+    end
   end
 
-  describe "GET /projects:id/repository/commits/:sha" do
+  describe "Get a single commit" do
     context "authorized user" do
       it "returns a commit by sha" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -95,7 +372,7 @@ describe API::API, api: true  do
       end
 
       it "returns status for CI" do
-        pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+        pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
         pipeline.update(status: 'success')
 
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -105,12 +382,12 @@ describe API::API, api: true  do
       end
 
       it "returns status for CI when pipeline is created" do
-        project.ensure_pipeline(project.repository.commit.sha, 'master')
+        project.ensure_pipeline('master', project.repository.commit.sha)
 
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['status']).to be_nil
+        expect(json_response['status']).to eq("created")
       end
     end
 
@@ -122,7 +399,7 @@ describe API::API, api: true  do
     end
   end
 
-  describe "GET /projects:id/repository/commits/:sha/diff" do
+  describe "Get the diff of a commit" do
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
@@ -149,7 +426,7 @@ describe API::API, api: true  do
     end
   end
 
-  describe 'GET /projects:id/repository/commits/:sha/comments' do
+  describe 'Get the comments of a commit' do
     context 'authorized user' do
       it 'returns merge_request comments' do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
@@ -174,7 +451,7 @@ describe API::API, api: true  do
     end
   end
 
-  describe 'POST /projects:id/repository/commits/:sha/comments' do
+  describe 'Post comment to commit' do
     context 'authorized user' do
       it 'returns comment' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
@@ -186,11 +463,12 @@ describe API::API, api: true  do
       end
 
       it 'returns the inline comment' do
-        post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
+        post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+
         expect(response).to have_http_status(201)
         expect(json_response['note']).to eq('My comment')
         expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
-        expect(json_response['line']).to eq(7)
+        expect(json_response['line']).to eq(1)
         expect(json_response['line_type']).to eq('new')
       end
 
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 7d8cc45327c73f339fb10eae5686f5f9dec652dd..65897edba7f885cbb79feda60de483b794bb4753 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -6,6 +6,7 @@ describe API::API, api: true  do
   let(:user)        { create(:user) }
   let(:admin)       { create(:admin) }
   let(:project)     { create(:project, creator_id: user.id) }
+  let(:project2)    { create(:project, creator_id: user.id) }
   let(:deploy_key)  { create(:deploy_key, public: true) }
 
   let!(:deploy_keys_project) do
@@ -96,6 +97,22 @@ describe API::API, api: true  do
         post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
       end.to change{ project.deploy_keys.count }.by(1)
     end
+
+    it 'returns an existing ssh key when attempting to add a duplicate' do
+      expect do
+        post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+      end.not_to change { project.deploy_keys.count }
+
+      expect(response).to have_http_status(201)
+    end
+
+    it 'joins an existing ssh key to a new project' do
+      expect do
+        post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+      end.to change { project2.deploy_keys.count }.by(1)
+
+      expect(response).to have_http_status(201)
+    end
   end
 
   describe 'DELETE /projects/:id/deploy_keys/:key_id' do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8fa8c66db6ce117981e30e88ea6fafafb51e7890
--- /dev/null
+++ b/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe API::API, 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
+
+  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/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 05e57905343bcfea499a17ceb987cf9b23ed8bde..1898b07835d68f0fb66ea01c4095e9c89bd22414 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -26,6 +26,7 @@ describe API::API, 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)
       end
     end
 
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2d1213df8a7b42b9cc6ab62cc4afd630c237fcbe..050d0dd082d1737f08b1fa17e9e281c0e17c6b68 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,6 +5,21 @@ describe API::API, api: true  do
   let(:user) { create(:user) }
   let!(:project) { create(:project, namespace: user.namespace ) }
   let(:file_path) { 'files/ruby/popen.rb' }
+  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.")
+  # This seems to be what git does under the hood. For example, this commit:
+  #
+  # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+  #
+  # results in this:
+  #
+  # $ git show --pretty
+  # ...
+  # Author: Foo Sr <foo@example.com>
+  # ...
+  let(:author_name) { FFaker::Name.name.chomp("\.") }
 
   before { project.team << [user, :developer] }
 
@@ -16,6 +31,7 @@ describe API::API, api: true  do
       }
 
       get api("/projects/#{project.id}/repository/files", user), params
+
       expect(response).to have_http_status(200)
       expect(json_response['file_path']).to eq(file_path)
       expect(json_response['file_name']).to eq('popen.rb')
@@ -25,6 +41,7 @@ describe API::API, api: true  do
 
     it "returns a 400 bad request if no params given" do
       get api("/projects/#{project.id}/repository/files", user)
+
       expect(response).to have_http_status(400)
     end
 
@@ -35,6 +52,7 @@ describe API::API, api: true  do
       }
 
       get api("/projects/#{project.id}/repository/files", user), params
+
       expect(response).to have_http_status(404)
     end
   end
@@ -51,12 +69,17 @@ describe API::API, api: true  do
 
     it "creates a new file in project repo" do
       post 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
 
     it "returns a 400 bad request if no params given" do
       post api("/projects/#{project.id}/repository/files", user)
+
       expect(response).to have_http_status(400)
     end
 
@@ -65,8 +88,22 @@ describe API::API, api: true  do
         and_return(false)
 
       post api("/projects/#{project.id}/repository/files", user), valid_params
+
       expect(response).to have_http_status(400)
     end
+
+    context "when specifying an author" 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
+
+        expect(response).to have_http_status(201)
+        last_commit = project.repository.commit.raw
+        expect(last_commit.author_email).to eq(author_email)
+        expect(last_commit.author_name).to eq(author_name)
+      end
+    end
   end
 
   describe "PUT /projects/:id/repository/files" do
@@ -81,14 +118,32 @@ describe API::API, api: true  do
 
     it "updates existing file in project repo" do
       put api("/projects/#{project.id}/repository/files", 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)
     end
 
     it "returns a 400 bad request if no params given" do
       put api("/projects/#{project.id}/repository/files", user)
+
       expect(response).to have_http_status(400)
     end
+
+    context "when specifying an author" 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
+
+        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)
+      end
+    end
   end
 
   describe "DELETE /projects/:id/repository/files" do
@@ -102,12 +157,17 @@ describe API::API, api: true  do
 
     it "deletes existing file in project repo" do
       delete api("/projects/#{project.id}/repository/files", 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)
     end
 
     it "returns a 400 bad request if no params given" do
       delete api("/projects/#{project.id}/repository/files", user)
+
       expect(response).to have_http_status(400)
     end
 
@@ -115,8 +175,22 @@ describe API::API, api: true  do
       allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
 
       delete api("/projects/#{project.id}/repository/files", user), valid_params
+
       expect(response).to have_http_status(400)
     end
+
+    context "when specifying an author" 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
+
+        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)
+      end
+    end
   end
 
   describe "POST /projects/:id/repository/files with binary file" do
@@ -143,6 +217,7 @@ describe API::API, api: true  do
 
     it "remains unchanged" do
       get api("/projects/#{project.id}/repository/files", 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)
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index f802fcd2d2e590607baabd4a40d7b143e9780e9f..e38d5745d44cd79ee50c14fc702360c54c09112c 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -6,13 +6,19 @@ describe API::API, api: true  do
   let(:user2) { create(:user) }
   let(:user3) { create(:user) }
   let(:admin) { create(:admin) }
+  let(:group) { create(:group) }
+  let(:group2) do
+    group = create(:group, name: 'group2_name')
+    group.add_owner(user2)
+    group
+  end
 
   let(:project) do
     create(:project, creator_id: user.id, namespace: user.namespace)
   end
 
   let(:project_user2) do
-    create(:project_member, :guest, user: user2, project: project)
+    create(:project_member, :reporter, user: user2, project: project)
   end
 
   describe 'POST /projects/fork/:id' do
@@ -22,6 +28,7 @@ describe API::API, api: true  do
     context 'when authenticated' do
       it 'forks if user has sufficient access to project' do
         post api("/projects/fork/#{project.id}", user2)
+
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq(project.name)
         expect(json_response['path']).to eq(project.path)
@@ -32,6 +39,7 @@ describe API::API, api: true  do
 
       it 'forks if user is admin' do
         post api("/projects/fork/#{project.id}", admin)
+
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq(project.name)
         expect(json_response['path']).to eq(project.path)
@@ -42,12 +50,14 @@ describe API::API, api: true  do
 
       it 'fails on missing project access for the project to fork' do
         post api("/projects/fork/#{project.id}", user3)
+
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Project Not Found')
       end
 
       it 'fails if forked project exists in the user namespace' do
         post api("/projects/fork/#{project.id}", user)
+
         expect(response).to have_http_status(409)
         expect(json_response['message']['name']).to eq(['has already been taken'])
         expect(json_response['message']['path']).to eq(['has already been taken'])
@@ -55,14 +65,70 @@ describe API::API, api: true  do
 
       it 'fails if project to fork from does not exist' do
         post api('/projects/fork/424242', user)
+
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Project Not Found')
       end
+
+      it 'forks with explicit own user namespace id' do
+        post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id
+
+        expect(response).to have_http_status(201)
+        expect(json_response['owner']['id']).to eq(user2.id)
+      end
+
+      it 'forks with explicit own user name as namespace' do
+        post api("/projects/fork/#{project.id}", user2), namespace: user2.username
+
+        expect(response).to have_http_status(201)
+        expect(json_response['owner']['id']).to eq(user2.id)
+      end
+
+      it 'forks to another user when admin' do
+        post api("/projects/fork/#{project.id}", admin), namespace: user2.username
+
+        expect(response).to have_http_status(201)
+        expect(json_response['owner']['id']).to eq(user2.id)
+      end
+
+      it 'fails if trying to fork to another user when not admin' do
+        post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'fails if trying to fork to non-existent namespace' do
+        post api("/projects/fork/#{project.id}", user2), namespace: 42424242
+
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 Target Namespace Not Found')
+      end
+
+      it 'forks to owned group' do
+        post api("/projects/fork/#{project.id}", user2), namespace: group2.name
+
+        expect(response).to have_http_status(201)
+        expect(json_response['namespace']['name']).to eq(group2.name)
+      end
+
+      it 'fails to fork to not owned group' do
+        post api("/projects/fork/#{project.id}", user2), namespace: group.name
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'forks to not owned group when admin' do
+        post api("/projects/fork/#{project.id}", admin), namespace: group.name
+
+        expect(response).to have_http_status(201)
+        expect(json_response['namespace']['name']).to eq(group.name)
+      end
     end
 
     context 'when unauthenticated' do
       it 'returns authentication error' do
         post api("/projects/fork/#{project.id}")
+
         expect(response).to have_http_status(401)
         expect(json_response['message']).to eq('401 Unauthorized')
       end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 4860b23c2ed7bfa2f68648b18c6ebac1bba93c64..b29a13b1d8b330c89d01838bf8674200ae57c294 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -37,7 +37,7 @@ describe API::API, api: true  do
       end
     end
 
-    context "when authenticated as  admin" do
+    context "when authenticated as admin" do
       it "admin: returns an array of all groups" do
         get api("/groups", admin)
         expect(response).to have_http_status(200)
@@ -45,6 +45,45 @@ describe API::API, api: true  do
         expect(json_response.length).to eq(2)
       end
     end
+
+    context "when using skip_groups in request" do
+      it "returns all groups excluding skipped groups" do
+        get 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
+      it "returns all groups you have access to" do
+        public_group = create :group, :public
+        get api("/groups", user1), all_available: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(public_group.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
+      it 'returns an array of groups the user owns' do
+        get api('/groups/owned', user2)
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(group2.name)
+      end
+    end
   end
 
   describe "GET /groups/:id" do
@@ -120,10 +159,11 @@ describe API::API, api: true  do
 
     context 'when authenticated as the group owner' do
       it 'updates the group' do
-        put api("/groups/#{group1.id}", user1), name: new_group_name
+        put 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
@@ -238,8 +278,14 @@ describe API::API, api: true  do
 
     context "when authenticated as user with group permissions" do
       it "creates group" do
-        post api("/groups", user3), attributes_for(:group)
+        group = attributes_for(:group, { request_access_enabled: false })
+
+        post 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 "does not create group, duplicate" do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index be52f88831f7078b468495bba8038632e4602349..f0f590b0331cb83dce89ad32bd5cdf6c510c38c0 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -38,6 +38,105 @@ describe API::API, api: true  do
     end
   end
 
+  describe 'GET /internal/two_factor_recovery_codes' do
+    it 'returns an error message when the key does not exist' do
+      post api('/internal/two_factor_recovery_codes'),
+           secret_token: secret_token,
+           key_id: 12345
+
+      expect(json_response['success']).to be_falsey
+      expect(json_response['message']).to eq('Could not find the given key')
+    end
+
+    it 'returns an error message when the key is a deploy key' do
+      deploy_key = create(:deploy_key)
+
+      post api('/internal/two_factor_recovery_codes'),
+           secret_token: secret_token,
+           key_id: deploy_key.id
+
+      expect(json_response['success']).to be_falsey
+      expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes')
+    end
+
+    it 'returns an error message when the user does not exist' do
+      key_without_user = create(:key, user: nil)
+
+      post api('/internal/two_factor_recovery_codes'),
+           secret_token: secret_token,
+           key_id: key_without_user.id
+
+      expect(json_response['success']).to be_falsey
+      expect(json_response['message']).to eq('Could not find a user for the given key')
+      expect(json_response['recovery_codes']).to be_nil
+    end
+
+    context 'when two-factor is enabled' do
+      it 'returns new recovery codes when the user exists' do
+        allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
+        allow_any_instance_of(User)
+          .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+
+        post api('/internal/two_factor_recovery_codes'),
+             secret_token: secret_token,
+             key_id: key.id
+
+        expect(json_response['success']).to be_truthy
+        expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+      end
+    end
+
+    context 'when two-factor is not enabled' do
+      it 'returns an error message' do
+        allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
+
+        post api('/internal/two_factor_recovery_codes'),
+             secret_token: secret_token,
+             key_id: key.id
+
+        expect(json_response['success']).to be_falsey
+        expect(json_response['recovery_codes']).to be_nil
+      end
+    end
+  end
+
+  describe "POST /internal/lfs_authenticate" do
+    before do
+      project.team << [user, :developer]
+    end
+
+    context 'user key' do
+      it 'returns the correct information about the key' do
+        lfs_auth(key.id, project)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['username']).to eq(user.username)
+        expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
+
+        expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+      end
+
+      it 'returns a 404 when the wrong key is provided' do
+        lfs_auth(nil, project)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'deploy key' do
+      let(:key) { create(:deploy_key) }
+
+      it 'returns the correct information about the key' do
+        lfs_auth(key.id, project)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
+        expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
+        expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+      end
+    end
+  end
+
   describe "GET /internal/discover" do
     it do
       get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -327,4 +426,13 @@ describe API::API, api: true  do
       protocol: 'ssh'
     )
   end
+
+  def lfs_auth(key_id, project)
+    post(
+      api("/internal/lfs_authenticate"),
+      key_id: key_id,
+      secret_token: secret_token,
+      project: project.path_with_namespace
+    )
+  end
 end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index a40e1a93b715abf29ec2d04c85dbee6af0b25ae0..beed53d1e5c5b9d0af972e025566db5e9ad675b6 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
 
 describe API::API, api: true  do
   include ApiHelpers
+
   let(:user)        { create(:user) }
   let(:user2)       { create(:user) }
   let(:non_member)  { create(:user) }
@@ -16,21 +17,27 @@ describe API::API, api: true  do
            assignee: user,
            project: project,
            state: :closed,
-           milestone: milestone
+           milestone: milestone,
+           created_at: generate(:issue_created_at),
+           updated_at: 3.hours.ago
   end
   let!(:confidential_issue) do
     create :issue,
            :confidential,
            project: project,
            author: author,
-           assignee: assignee
+           assignee: assignee,
+           created_at: generate(:issue_created_at),
+           updated_at: 2.hours.ago
   end
   let!(:issue) do
     create :issue,
            author: user,
            assignee: user,
            project: project,
-           milestone: milestone
+           milestone: milestone,
+           created_at: generate(:issue_created_at),
+           updated_at: 1.hour.ago
   end
   let!(:label) do
     create(:label, title: 'label', color: '#FFAABB', project: project)
@@ -61,6 +68,7 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.first['title']).to eq(issue.title)
+        expect(json_response.last).to have_key('web_url')
       end
 
       it "adds pagination headers and keep query params" do
@@ -133,6 +141,42 @@ describe API::API, api: true  do
         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)
+        response_dates = json_response.map { |issue| issue['created_at'] }
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_dates).to eq(response_dates.sort.reverse)
+      end
+
+      it 'sorts ascending when requested' do
+        get api('/issues?sort=asc', user)
+        response_dates = json_response.map { |issue| issue['created_at'] }
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_dates).to eq(response_dates.sort)
+      end
+
+      it 'sorts by updated_at descending when requested' do
+        get api('/issues?order_by=updated_at', user)
+        response_dates = json_response.map { |issue| issue['updated_at'] }
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_dates).to eq(response_dates.sort.reverse)
+      end
+
+      it 'sorts by updated_at ascending when requested' do
+        get api('/issues?order_by=updated_at&sort=asc', user)
+        response_dates = json_response.map { |issue| issue['updated_at'] }
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(response_dates).to eq(response_dates.sort)
+      end
     end
   end
 
@@ -145,21 +189,24 @@ describe API::API, api: true  do
              assignee: user,
              project: group_project,
              state: :closed,
-             milestone: group_milestone
+             milestone: group_milestone,
+             updated_at: 3.hours.ago
     end
     let!(:group_confidential_issue) do
       create :issue,
              :confidential,
              project: group_project,
              author: author,
-             assignee: assignee
+             assignee: assignee,
+             updated_at: 2.hours.ago
     end
     let!(:group_issue) do
       create :issue,
              author: user,
              assignee: user,
              project: group_project,
-             milestone: group_milestone
+             milestone: group_milestone,
+             updated_at: 1.hour.ago
     end
     let!(:group_label) do
       create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
@@ -276,6 +323,42 @@ describe API::API, api: true  do
       expect(json_response.length).to eq(1)
       expect(json_response.first['id']).to eq(group_closed_issue.id)
     end
+
+    it 'sorts by created_at descending by default' do
+      get api(base_url, user)
+      response_dates = json_response.map { |issue| issue['created_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort.reverse)
+    end
+
+    it 'sorts ascending when requested' do
+      get api("#{base_url}?sort=asc", user)
+      response_dates = json_response.map { |issue| issue['created_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort)
+    end
+
+    it 'sorts by updated_at descending when requested' do
+      get api("#{base_url}?order_by=updated_at", user)
+      response_dates = json_response.map { |issue| issue['updated_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort.reverse)
+    end
+
+    it 'sorts by updated_at ascending when requested' do
+      get api("#{base_url}?order_by=updated_at&sort=asc", user)
+      response_dates = json_response.map { |issue| issue['updated_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort)
+    end
   end
 
   describe "GET /projects/:id/issues" do
@@ -384,6 +467,42 @@ describe API::API, api: true  do
       expect(json_response.length).to eq(1)
       expect(json_response.first['id']).to eq(closed_issue.id)
     end
+
+    it 'sorts by created_at descending by default' do
+      get api("#{base_url}/issues", user)
+      response_dates = json_response.map { |issue| issue['created_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort.reverse)
+    end
+
+    it 'sorts ascending when requested' do
+      get api("#{base_url}/issues?sort=asc", user)
+      response_dates = json_response.map { |issue| issue['created_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort)
+    end
+
+    it 'sorts by updated_at descending when requested' do
+      get api("#{base_url}/issues?order_by=updated_at", user)
+      response_dates = json_response.map { |issue| issue['updated_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort.reverse)
+    end
+
+    it 'sorts by updated_at ascending when requested' do
+      get api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+      response_dates = json_response.map { |issue| issue['updated_at'] }
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(response_dates).to eq(response_dates.sort)
+    end
   end
 
   describe "GET /projects/:id/issues/:issue_id" do
@@ -403,6 +522,7 @@ describe API::API, api: true  do
       expect(json_response['milestone']).to be_a Hash
       expect(json_response['assignee']).to be_a Hash
       expect(json_response['author']).to be_a Hash
+      expect(json_response['confidential']).to be_falsy
     end
 
     it "returns a project issue by id" do
@@ -468,13 +588,63 @@ describe API::API, api: true  do
   end
 
   describe "POST /projects/:id/issues" do
-    it "creates a new project issue" do
+    it 'creates a new project issue' do
       post api("/projects/#{project.id}/issues", user),
         title: 'new issue', labels: 'label, label2'
+
       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['confidential']).to be_falsy
+    end
+
+    it 'creates a new confidential project issue' do
+      post api("/projects/#{project.id}/issues", user),
+        title: 'new issue', confidential: true
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('new issue')
+      expect(json_response['confidential']).to be_truthy
+    end
+
+    it 'creates a new confidential project issue with a different param' do
+      post api("/projects/#{project.id}/issues", user),
+        title: 'new issue', confidential: 'y'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('new issue')
+      expect(json_response['confidential']).to be_truthy
+    end
+
+    it 'creates a public issue when confidential param is false' do
+      post api("/projects/#{project.id}/issues", user),
+        title: 'new issue', confidential: false
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('new issue')
+      expect(json_response['confidential']).to be_falsy
+    end
+
+    it 'creates a public issue when confidential param is invalid' do
+      post api("/projects/#{project.id}/issues", user),
+        title: 'new issue', confidential: 'foo'
+
+      expect(response).to have_http_status(201)
+      expect(json_response['title']).to eq('new issue')
+      expect(json_response['confidential']).to be_falsy
+    end
+
+    it "sends notifications for subscribers of newly added labels" do
+      label = project.labels.first
+      label.toggle_subscription(user2)
+
+      perform_enqueued_jobs do
+        post api("/projects/#{project.id}/issues", user),
+          title: 'new issue', labels: label.title
+      end
+
+      should_email(user2)
     end
 
     it "returns a 400 bad request if title not given" do
@@ -524,7 +694,7 @@ describe API::API, api: true  do
           title: 'new issue', labels: 'label, label2', created_at: creation_time
 
         expect(response).to have_http_status(201)
-        expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
+        expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
       end
     end
   end
@@ -618,6 +788,30 @@ describe API::API, api: true  do
         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),
+          confidential: true
+
+        expect(response).to have_http_status(200)
+        expect(json_response['confidential']).to be_truthy
+      end
+
+      it 'makes a confidential issue public' do
+        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+          confidential: false
+
+        expect(response).to have_http_status(200)
+        expect(json_response['confidential']).to be_falsy
+      end
+
+      it 'does not update a confidential issue with wrong confidential flag' do
+        put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+          confidential: 'foo'
+
+        expect(response).to have_http_status(200)
+        expect(json_response['confidential']).to be_truthy
+      end
     end
   end
 
@@ -632,6 +826,18 @@ describe API::API, api: true  do
       expect(json_response['labels']).to eq([label.title])
     end
 
+    it "sends notifications for subscribers of newly added labels when issue is updated" do
+      label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+      label.toggle_subscription(user2)
+
+      perform_enqueued_jobs do
+        put api("/projects/#{project.id}/issues/#{issue.id}", user),
+          title: 'updated title', labels: label.title
+      end
+
+      should_email(user2)
+    end
+
     it 'removes all labels' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: ''
@@ -689,7 +895,7 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
 
         expect(json_response['labels']).to include 'label3'
-        expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
+        expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
       end
     end
   end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 83789223019c76ffdbb904c22f6e918c0367f0ea..5d84976c9c35b939d5c614d5072f2c8830e1265f 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -6,18 +6,38 @@ describe API::API, api: true  do
   let(:user) { create(:user) }
   let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
   let!(:label1) { create(:label, title: 'label1', project: project) }
+  let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
 
   before do
     project.team << [user, :master]
   end
 
   describe 'GET /projects/:id/labels' do
-    it 'returns project labels' do
+    it 'returns all available labels to the project' do
+      group = create(:group)
+      group_label = create(:group_label, title: 'feature', group: group)
+      project.update(group: group)
+      expected_keys = [
+        'id', 'name', 'color', 'description',
+        'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
+        'subscribed', 'priority'
+      ]
+
       get api("/projects/#{project.id}/labels", 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(label1.name)
+      expect(json_response.size).to eq(3)
+      expect(json_response.first.keys).to match_array expected_keys
+      expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
+      expect(json_response.last['name']).to eq(label1.name)
+      expect(json_response.last['color']).to be_present
+      expect(json_response.last['description']).to be_nil
+      expect(json_response.last['open_issues_count']).to eq(0)
+      expect(json_response.last['closed_issues_count']).to eq(0)
+      expect(json_response.last['open_merge_requests_count']).to eq(0)
+      expect(json_response.last['priority']).to be_nil
+      expect(json_response.last['subscribed']).to be_falsey
     end
   end
 
@@ -26,21 +46,39 @@ describe API::API, api: true  do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAABB',
-           description: 'test'
+           description: 'test',
+           priority: 2
+
       expect(response).to have_http_status(201)
       expect(json_response['name']).to eq('Foo')
       expect(json_response['color']).to eq('#FFAABB')
       expect(json_response['description']).to eq('test')
+      expect(json_response['priority']).to eq(2)
     end
 
     it 'returns created label when only required params' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo & Bar',
            color: '#FFAABB'
+
+      expect(response.status).to eq(201)
+      expect(json_response['name']).to eq('Foo & Bar')
+      expect(json_response['color']).to eq('#FFAABB')
+      expect(json_response['description']).to be_nil
+      expect(json_response['priority']).to be_nil
+    end
+
+    it 'creates a prioritized label' do
+      post api("/projects/#{project.id}/labels", user),
+           name: 'Foo & Bar',
+           color: '#FFAABB',
+           priority: 3
+
       expect(response.status).to eq(201)
       expect(json_response['name']).to eq('Foo & Bar')
       expect(json_response['color']).to eq('#FFAABB')
       expect(json_response['description']).to be_nil
+      expect(json_response['priority']).to eq(3)
     end
 
     it 'returns a 400 bad request if name not given' do
@@ -77,7 +115,29 @@ describe API::API, api: true  do
       expect(json_response['message']['title']).to eq(['is invalid'])
     end
 
-    it 'returns 409 if label already exists' do
+    it 'returns 409 if label already exists in group' do
+      group = create(:group)
+      group_label = create(:group_label, group: group)
+      project.update(group: group)
+
+      post api("/projects/#{project.id}/labels", user),
+           name: group_label.name,
+           color: '#FFAABB'
+
+      expect(response).to have_http_status(409)
+      expect(json_response['message']).to eq('Label already exists')
+    end
+
+    it 'returns 400 for invalid priority' do
+      post api("/projects/#{project.id}/labels", user),
+           name: 'Foo',
+           color: '#FFAAFFFF',
+           priority: 'foo'
+
+      expect(response).to have_http_status(400)
+    end
+
+    it 'returns 409 if label already exists in project' do
       post api("/projects/#{project.id}/labels", user),
            name: 'label1',
            color: '#FFAABB'
@@ -137,11 +197,43 @@ describe API::API, api: true  do
 
     it 'returns 200 if description is changed' do
       put api("/projects/#{project.id}/labels", user),
-          name: 'label1',
+          name: 'bug',
           description: 'test'
+
       expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq(label1.name)
+      expect(json_response['name']).to eq(priority_label.name)
       expect(json_response['description']).to eq('test')
+      expect(json_response['priority']).to eq(3)
+    end
+
+    it 'returns 200 if priority is changed' do
+      put api("/projects/#{project.id}/labels", user),
+           name: 'bug',
+           priority: 10
+
+      expect(response.status).to eq(200)
+      expect(json_response['name']).to eq(priority_label.name)
+      expect(json_response['priority']).to eq(10)
+    end
+
+    it 'returns 200 if a priority is added' do
+      put api("/projects/#{project.id}/labels", user),
+           name: 'label1',
+           priority: 3
+
+      expect(response.status).to eq(200)
+      expect(json_response['name']).to eq(label1.name)
+      expect(json_response['priority']).to eq(3)
+    end
+
+    it 'returns 200 if the priority is removed' do
+      put api("/projects/#{project.id}/labels", user),
+          name: priority_label.name,
+          priority: nil
+
+      expect(response.status).to eq(200)
+      expect(json_response['name']).to eq(priority_label.name)
+      expect(json_response['priority']).to be_nil
     end
 
     it 'returns 404 if label does not exist' do
@@ -154,14 +246,14 @@ describe API::API, api: true  do
     it 'returns 400 if no label name given' do
       put api("/projects/#{project.id}/labels", user), new_name: 'label2'
       expect(response).to have_http_status(400)
-      expect(json_response['message']).to eq('400 (Bad request) "name" not given')
+      expect(json_response['error']).to eq('name is missing')
     end
 
     it 'returns 400 if no new parameters given' do
       put api("/projects/#{project.id}/labels", user), name: 'label1'
       expect(response).to have_http_status(400)
-      expect(json_response['message']).to eq('Required parameters '\
-                                         '"new_name" or "color" missing')
+      expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
+                                           'at least one parameter must be provided')
     end
 
     it 'returns 400 for invalid name' do
@@ -188,6 +280,14 @@ describe API::API, api: true  do
       expect(response).to have_http_status(400)
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
+
+    it 'returns 400 for invalid priority' do
+      post api("/projects/#{project.id}/labels", user),
+           name: 'Foo',
+           priority: 'foo'
+
+      expect(response).to have_http_status(400)
+    end
   end
 
   describe "POST /projects/:id/labels/:label_id/subscription" do
diff --git a/spec/requests/api/license_templates_spec.rb b/spec/requests/api/license_templates_spec.rb
deleted file mode 100644
index 9a1894d63a2d8d73397792bed02f8ef78d97de00..0000000000000000000000000000000000000000
--- a/spec/requests/api/license_templates_spec.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-
-  describe 'Entity' do
-    before { get api('/licenses/mit') }
-
-    it { expect(json_response['key']).to eq('mit') }
-    it { expect(json_response['name']).to eq('MIT License') }
-    it { expect(json_response['nickname']).to be_nil }
-    it { expect(json_response['popular']).to be true }
-    it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') }
-    it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') }
-    it { expect(json_response['description']).to include('A permissive license that is short and to the point.') }
-    it { expect(json_response['conditions']).to eq(%w[include-copyright]) }
-    it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) }
-    it { expect(json_response['limitations']).to eq(%w[no-liability]) }
-    it { expect(json_response['content']).to include('The MIT License (MIT)') }
-  end
-
-  describe 'GET /licenses' do
-    it 'returns a list of available license templates' do
-      get api('/licenses')
-
-      expect(response).to have_http_status(200)
-      expect(json_response).to be_an Array
-      expect(json_response.size).to eq(15)
-      expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
-    end
-
-    describe 'the popular parameter' do
-      context 'with popular=1' do
-        it 'returns a list of available popular license templates' do
-          get api('/licenses?popular=1')
-
-          expect(response).to have_http_status(200)
-          expect(json_response).to be_an Array
-          expect(json_response.size).to eq(3)
-          expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
-        end
-      end
-    end
-  end
-
-  describe 'GET /licenses/:key' do
-    context 'with :project and :fullname given' do
-      before do
-        get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
-      end
-
-      context 'for the mit license' do
-        let(:license_type) { 'mit' }
-
-        it 'returns the license text' do
-          expect(json_response['content']).to include('The MIT License (MIT)')
-        end
-
-        it 'replaces placeholder values' do
-          expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
-        end
-      end
-
-      context 'for the agpl-3.0 license' do
-        let(:license_type) { 'agpl-3.0' }
-
-        it 'returns the license text' do
-          expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
-        end
-
-        it 'replaces placeholder values' do
-          expect(json_response['content']).to include('My Awesome Project')
-          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
-        end
-      end
-
-      context 'for the gpl-3.0 license' do
-        let(:license_type) { 'gpl-3.0' }
-
-        it 'returns the license text' do
-          expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
-        end
-
-        it 'replaces placeholder values' do
-          expect(json_response['content']).to include('My Awesome Project')
-          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
-        end
-      end
-
-      context 'for the gpl-2.0 license' do
-        let(:license_type) { 'gpl-2.0' }
-
-        it 'returns the license text' do
-          expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
-        end
-
-        it 'replaces placeholder values' do
-          expect(json_response['content']).to include('My Awesome Project')
-          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
-        end
-      end
-
-      context 'for the apache-2.0 license' do
-        let(:license_type) { 'apache-2.0' }
-
-        it 'returns the license text' do
-          expect(json_response['content']).to include('Apache License')
-        end
-
-        it 'replaces placeholder values' do
-          expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
-        end
-      end
-
-      context 'for an uknown license' do
-        let(:license_type) { 'muth-over9000' }
-
-        it 'returns a 404' do
-          expect(response).to have_http_status(404)
-        end
-      end
-    end
-
-    context 'with no :fullname given' do
-      context 'with an authenticated user' do
-        let(:user) { create(:user) }
-
-        it 'replaces the copyright owner placeholder with the name of the current user' do
-          get api('/licenses/mit', user)
-
-          expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
-        end
-      end
-    end
-  end
-end
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..391fc13a380f56ff2fd997f221685e1f166d4146
--- /dev/null
+++ b/spec/requests/api/lint_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe API::Lint, api: true do
+  include ApiHelpers
+
+  describe 'POST /ci/lint' do
+    context 'with valid .gitlab-ci.yaml content' do
+      let(:yaml_content) do
+        File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+      end
+
+      it 'passes validation' do
+        post api('/ci/lint'), { content: yaml_content }
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Hash
+        expect(json_response['status']).to eq('valid')
+        expect(json_response['errors']).to eq([])
+      end
+    end
+
+    context 'with an invalid .gitlab_ci.yml' do
+      it 'responds with errors about invalid syntax' do
+        post api('/ci/lint'), { content: 'invalid content' }
+
+        expect(response).to have_http_status(200)
+        expect(json_response['status']).to eq('invalid')
+        expect(json_response['errors']).to eq(['Invalid configuration format'])
+      end
+
+      it "responds with errors about invalid configuration" do
+        post api('/ci/lint'), { content: '{ image: "ruby:2.1",  services: ["postgres"] }' }
+
+        expect(response).to have_http_status(200)
+        expect(json_response['status']).to eq('invalid')
+        expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
+      end
+    end
+
+    context 'without the content parameter' do
+      it 'responds with validation error about missing content' do
+        post api('/ci/lint')
+
+        expect(response).to have_http_status(400)
+        expect(json_response['error']).to eq('content is missing')
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index a56ee30f7b15eb0bc8b9000cdb14491e568eff35..493c0a893d10fc561120c4bf06d391e23bb4ebf6 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -30,20 +30,29 @@ describe API::Members, api: true  do
         let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
       end
 
-      context 'when authenticated as a non-member' do
-        %i[access_requester stranger].each do |type|
-          context "as a #{type}" do
-            it 'returns 200' do
-              user = public_send(type)
-              get api("/#{source_type.pluralize}/#{source.id}/members", user)
+      %i[master developer access_requester stranger].each do |type|
+        context "when authenticated as a #{type}" do
+          it 'returns 200' do
+            user = public_send(type)
+            get api("/#{source_type.pluralize}/#{source.id}/members", user)
 
-              expect(response).to have_http_status(200)
-              expect(json_response.size).to eq(2)
-            end
+            expect(response).to have_http_status(200)
+            expect(json_response.size).to eq(2)
+            expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
           end
         end
       end
 
+      it 'does not return invitees' do
+        create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
+
+        get api("/#{source_type.pluralize}/#{source.id}/members", developer)
+
+        expect(response).to have_http_status(200)
+        expect(json_response.size).to eq(2)
+        expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+      end
+
       it 'finds members with query string' do
         get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
 
@@ -88,7 +97,10 @@ describe API::Members, api: true  do
   shared_examples 'POST /:sources/:id/members' do |source_type|
     context "with :sources == #{source_type.pluralize}" do
       it_behaves_like 'a 404 response when source is private' do
-        let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+        let(:route) do
+          post api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+               user_id: access_requester.id, access_level: Member::MASTER
+        end
       end
 
       context 'when authenticated as a non-member or member with insufficient rights' do
@@ -96,7 +108,8 @@ describe API::Members, api: true  do
           context "as a #{type}" do
             it 'returns 403' do
               user = public_send(type)
-              post api("/#{source_type.pluralize}/#{source.id}/members", user)
+              post api("/#{source_type.pluralize}/#{source.id}/members", user),
+                   user_id: access_requester.id, access_level: Member::MASTER
 
               expect(response).to have_http_status(403)
             end
@@ -122,12 +135,13 @@ describe API::Members, api: true  do
         it 'creates a new member' do
           expect do
             post api("/#{source_type.pluralize}/#{source.id}/members", master),
-                 user_id: stranger.id, access_level: Member::DEVELOPER
+                 user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
 
             expect(response).to have_http_status(201)
           end.to change { source.members.count }.by(1)
           expect(json_response['id']).to eq(stranger.id)
           expect(json_response['access_level']).to eq(Member::DEVELOPER)
+          expect(json_response['expires_at']).to eq('2016-08-05')
         end
       end
 
@@ -164,7 +178,10 @@ describe API::Members, api: true  do
   shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
     context "with :sources == #{source_type.pluralize}" do
       it_behaves_like 'a 404 response when source is private' do
-        let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+        let(:route) do
+          put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+              access_level: Member::MASTER
+        end
       end
 
       context 'when authenticated as a non-member or member with insufficient rights' do
@@ -172,7 +189,8 @@ describe API::Members, api: true  do
           context "as a #{type}" do
             it 'returns 403' do
               user = public_send(type)
-              put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+              put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+                  access_level: Member::MASTER
 
               expect(response).to have_http_status(403)
             end
@@ -183,11 +201,12 @@ describe API::Members, api: true  do
       context 'when authenticated as a master/owner' do
         it 'updates the member' do
           put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
-              access_level: Member::MASTER
+              access_level: Member::MASTER, expires_at: '2016-08-05'
 
           expect(response).to have_http_status(200)
           expect(json_response['id']).to eq(developer.id)
           expect(json_response['access_level']).to eq(Member::MASTER)
+          expect(json_response['expires_at']).to eq('2016-08-05')
         end
       end
 
@@ -309,4 +328,15 @@ describe API::Members, api: true  do
   it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
     let(:source) { group }
   end
+
+  context 'Adding owner to project' do
+    it 'returns 403' do
+      expect do
+        post api("/projects/#{project.id}/members", master),
+             user_id: stranger.id, access_level: Member::OWNER
+
+        expect(response).to have_http_status(422)
+      end.to change { project.members.count }.by(0)
+    end
+  end
 end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..131c2d406ea46b7bfd496086539d10b529c2e768
--- /dev/null
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::API, '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 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 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 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 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/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 617600d617344f2922e54c2fd393ec43010bef75..bae4fa11ec2e89041e73f67b54ded92324828cbd 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,13 +9,13 @@ describe API::API, api: true  do
   let!(:project)    { create(:project, creator_id: user.id, namespace: user.namespace) }
   let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
   let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
-  let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
+  let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
   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") }
   let(:milestone)   { create(:milestone, title: '1.0.0', project: project) }
 
   before do
-    project.team << [user, :reporters]
+    project.team << [user, :reporter]
   end
 
   describe "GET /projects/:id/merge_requests" do
@@ -33,6 +33,14 @@ describe API::API, api: true  do
         expect(json_response).to be_an Array
         expect(json_response.length).to eq(3)
         expect(json_response.last['title']).to eq(merge_request.title)
+        expect(json_response.last).to have_key('web_url')
+        expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+        expect(json_response.last['merge_commit_sha']).to be_nil
+        expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+        expect(json_response.first['title']).to eq(merge_request_merged.title)
+        expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+        expect(json_response.first['merge_commit_sha']).not_to be_nil
+        expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
       end
 
       it "returns an array of all merge_requests" do
@@ -178,14 +186,14 @@ describe API::API, api: true  do
   end
 
   describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
-    context 'valid merge request' do
-      before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) }
-      let(:commit) { merge_request.commits.first }
+    it 'returns a 200 when merge request is valid' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+      commit = merge_request.commits.first
 
-      it { expect(response.status).to eq 200 }
-      it { expect(json_response.size).to eq(merge_request.commits.size) }
-      it { expect(json_response.first['id']).to eq(commit.id) }
-      it { expect(json_response.first['title']).to eq(commit.title) }
+      expect(response.status).to eq 200
+      expect(json_response.size).to eq(merge_request.commits.size)
+      expect(json_response.first['id']).to eq(commit.id)
+      expect(json_response.first['title']).to eq(commit.title)
     end
 
     it 'returns a 404 when merge_request_id not found' do
@@ -291,7 +299,7 @@ describe API::API, api: true  do
       let!(:unrelated_project) { create(:project,  namespace: create(:user).namespace, creator_id: user2.id) }
 
       before :each do |each|
-        fork_project.team << [user2, :reporters]
+        fork_project.team << [user2, :reporter]
       end
 
       it "returns merge_request" do
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index d6a0c656e7495dccdf43e21145a41d169962487e..62327f64e5099b69ae63d73105c1162227afc897 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
 describe API::API, api: true  do
   include ApiHelpers
   let(:user) { create(:user) }
-  let!(:project) { create(:project, namespace: user.namespace ) }
+  let!(:project) { create(:empty_project, namespace: user.namespace ) }
   let!(:closed_milestone) { create(:closed_milestone, project: project) }
   let!(:milestone) { create(:milestone, project: project) }
 
@@ -12,6 +12,7 @@ describe API::API, api: true  do
   describe 'GET /projects/:id/milestones' do
     it 'returns project milestones' do
       get 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)
@@ -19,6 +20,7 @@ describe API::API, api: true  do
 
     it 'returns a 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones")
+
       expect(response).to have_http_status(401)
     end
 
@@ -44,6 +46,7 @@ describe API::API, api: true  do
   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)
@@ -60,11 +63,13 @@ describe API::API, api: true  do
 
     it 'returns 401 error if user not authenticated' do
       get 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 api("/projects/#{project.id}/milestones/1234", user)
+
       expect(response).to have_http_status(404)
     end
   end
@@ -72,6 +77,7 @@ describe API::API, api: true  do
   describe 'POST /projects/:id/milestones' do
     it 'creates a new project milestone' do
       post 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
@@ -80,6 +86,7 @@ describe API::API, api: true  do
     it 'creates a new project milestone with description and due date' do
       post api("/projects/#{project.id}/milestones", user),
         title: 'new milestone', description: 'release', due_date: '2013-03-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')
@@ -87,21 +94,48 @@ describe API::API, api: true  do
 
     it 'returns a 400 error if title is missing' do
       post 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 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 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 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 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 api("/projects/#{project.id}/milestones/1234", user),
         title: 'updated title'
+
       expect(response).to have_http_status(404)
     end
   end
@@ -131,6 +165,7 @@ describe API::API, api: true  do
     end
     it 'returns project issues for a particular milestone' do
       get 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)
@@ -138,11 +173,12 @@ describe API::API, api: true  do
 
     it 'returns a 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+
       expect(response).to have_http_status(401)
     end
 
     describe 'confidential issues' do
-      let(:public_project) { create(:project, :public) }
+      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) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 737fa14cbb0c366a1ee1ad8b6fb5bc04f33fc2c1..0124b7271b3d6a17d06c3b49fb3a8e22f4a7498e 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -25,7 +25,7 @@ describe API::API, api: true  do
   let!(:cross_reference_note) do
     create :note,
     noteable: ext_issue, project: ext_proj,
-    note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+    note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
     system: true
   end
 
@@ -217,7 +217,27 @@ describe API::API, api: true  do
           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_within(1.second).of(creation_time)
+          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 'returns an award emoji' 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
+        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 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
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6d8a5ee95407914659bace6e3055fdb30421a10
--- /dev/null
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let!(:group) { create(:group) }
+  let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) }
+
+  describe "GET /notification_settings" do
+    it "returns global notification settings for the current user" do
+      get api("/notification_settings", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a Hash
+      expect(json_response['notification_email']).to eq(user.notification_email)
+      expect(json_response['level']).to eq(user.global_notification_setting.level)
+    end
+  end
+
+  describe "PUT /notification_settings" do
+    let(:email) { create(:email, user: user) }
+
+    it "updates global notification settings for the current user" do
+      put api("/notification_settings", user), { level: 'watch', notification_email: email.email }
+
+      expect(response).to have_http_status(200)
+      expect(json_response['notification_email']).to eq(email.email)
+      expect(user.reload.notification_email).to eq(email.email)
+      expect(json_response['level']).to eq(user.reload.global_notification_setting.level)
+    end
+  end
+
+  describe "PUT /notification_settings" do
+    it "fails on non-user email address" do
+      put api("/notification_settings", user), { notification_email: 'invalid@example.com' }
+
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  describe "GET /groups/:id/notification_settings" do
+    it "returns group level notification settings for the current user" do
+      get api("/groups/#{group.id}/notification_settings", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a Hash
+      expect(json_response['level']).to eq(user.notification_settings_for(group).level)
+    end
+  end
+
+  describe "PUT /groups/:id/notification_settings" do
+    it "updates group level notification settings for the current user" do
+      put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' }
+
+      expect(response).to have_http_status(200)
+      expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level)
+    end
+  end
+
+  describe "GET /projects/:id/notification_settings" do
+    it "returns project level notification settings for the current user" do
+      get api("/projects/#{project.id}/notification_settings", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a Hash
+      expect(json_response['level']).to eq(user.notification_settings_for(project).level)
+    end
+  end
+
+  describe "PUT /projects/:id/notification_settings" do
+    it "updates project level notification settings for the current user" do
+      put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true }
+
+      expect(response).to have_http_status(200)
+      expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level)
+      expect(json_response['events']['new_note']).to eq(true)
+      expect(json_response['events']['new_issue']).to eq(false)
+    end
+  end
+
+  describe "PUT /projects/:id/notification_settings" do
+    it "fails on invalid level" do
+      put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' }
+
+      expect(response).to have_http_status(400)
+    end
+  end
+end
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e2cc50e5917b63fefee049fda8b2b4e1fac4662
--- /dev/null
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe API::API, api: true  do
+  include ApiHelpers
+
+  context 'Resource Owner Password Credentials' do
+    def request_oauth_token(user)
+      post '/oauth/token', username: user.username, password: user.password, grant_type: 'password'
+    end
+
+    context 'when user has 2FA enabled' do
+      it 'does not create an access token' do
+        user = create(:user, :two_factor)
+
+        request_oauth_token(user)
+
+        expect(response).to have_http_status(401)
+        expect(json_response['error']).to eq('invalid_grant')
+      end
+    end
+
+    context 'when user does not have 2FA enabled' do
+      it 'creates an access token' do
+        user = create(:user)
+
+        request_oauth_token(user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['access_token']).not_to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7011bdc9ec00a488156e33d33e6f4e052fa1662e
--- /dev/null
+++ b/spec/requests/api/pipelines_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:non_member)  { create(:user) }
+  let(:project)     { create(:project, creator_id: user.id) }
+
+  let!(:pipeline) do
+    create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+                               ref: project.default_branch)
+  end
+
+  before { project.team << [user, :master] }
+
+  describe 'GET /projects/:id/pipelines ' do
+    it_behaves_like 'a paginated resources' do
+      let(:request) { get api("/projects/#{project.id}/pipelines", user) }
+    end
+
+    context 'authorized user' do
+      it 'returns project pipelines' do
+        get 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
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not return project pipelines' do
+        get 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 'GET /projects/:id/pipelines/:pipeline_id' do
+    context 'authorized user' do
+      it 'returns project pipelines' do
+        get 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 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
+    end
+
+    context 'unauthorized user' do
+      it 'should not return a project pipeline' do
+        get 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 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 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 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 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/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 914e88c94878d0cfa3b55b52f1496a51f2501a4b..5f39329a1b821c7392d01c81f97c216d4fb108e1 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -34,6 +34,7 @@ describe API::API, 'ProjectHooks', api: true do
         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
@@ -57,6 +58,9 @@ describe API::API, '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['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
 
@@ -84,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do
       expect do
         post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_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)
@@ -93,7 +98,26 @@ describe API::API, 'ProjectHooks', api: true do
       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(false)
       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 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
@@ -118,9 +142,25 @@ describe API::API, '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['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 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 api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
       expect(response).to have_http_status(404)
@@ -155,9 +195,10 @@ describe API::API, 'ProjectHooks', api: true do
       expect(response).to have_http_status(404)
     end
 
-    it "returns a 405 error if hook id not given" do
+    it "returns a 404 error if hook id not given" do
       delete api("/projects/#{project.id}/hooks", user)
-      expect(response).to have_http_status(405)
+
+      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
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 42757ff21b081d984dc0486fff3c32cb8dad32b0..01148f0a05ea427dfca7d71a09bd0baeb5af2204 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -30,6 +30,7 @@ describe API::API, api: true do
       expect(response).to have_http_status(200)
       expect(json_response.size).to eq(3)
       expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+      expect(json_response.last).to have_key('web_url')
     end
 
     it 'hides private snippets from regular user' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 4742b3d0e374e69e609318fbc0f7a80783073b88..d6e9fd2c4b27084486372e181023f5c90e500013 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -73,7 +73,7 @@ describe API::API, api: true  do
       end
 
       it 'does not include open_issues_count' do
-        project.update_attributes( { issues_enabled: false } )
+        project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
 
         get api('/projects', user)
         expect(response.status).to eq 200
@@ -175,6 +175,60 @@ describe API::API, api: true  do
     end
   end
 
+  describe 'GET /projects/owned' do
+    before do
+      project3
+      project4
+    end
+
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        get api('/projects/owned')
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when authenticated as project owner' do
+      it 'returns an array of projects the user owns' do
+        get api('/projects/owned', user4)
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(project4.name)
+        expect(json_response.first['owner']['username']).to eq(user4.username)
+      end
+    end
+  end
+
+  describe 'GET /projects/visible' do
+    let(:public_project) { create(:project, :public) }
+
+    before do
+      public_project
+      project
+      project2
+      project3
+      project4
+    end
+
+    it 'returns the projects viewable by the user' do
+      get api('/projects/visible', user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.map { |project| project['id'] }).
+        to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+    end
+
+    it 'shows only public projects when the user only has access to those' do
+      get api('/projects/visible', user2)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.map { |project| project['id'] }).
+        to contain_exactly(public_project.id)
+    end
+  end
+
   describe 'GET /projects/starred' do
     let(:public_project) { create(:project, :public) }
 
@@ -224,14 +278,24 @@ describe API::API, api: true  do
         description: FFaker::Lorem.sentence,
         issues_enabled: false,
         merge_requests_enabled: false,
-        wiki_enabled: false
+        wiki_enabled: false,
+        only_allow_merge_if_build_succeeds: false,
+        request_access_enabled: true,
+        only_allow_merge_if_all_discussions_are_resolved: false
       })
 
       post api('/projects', user), project
 
       project.each_pair do |k, v|
+        next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
         expect(json_response[k.to_s]).to eq(v)
       end
+
+      # Check feature permissions attributes
+      project = Project.find_by_path(project[:path])
+      expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+      expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
+      expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
     end
 
     it 'sets a project as public' do
@@ -276,6 +340,34 @@ describe API::API, api: true  do
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 })
+      post api('/projects', user), project
+      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
+      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+      post api('/projects', user), project
+      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+    end
+
+    it 'sets a project as allowing merge even if discussions are unresolved' do
+      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+      post api('/projects', user), project
+
+      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+    end
+
+    it 'sets a project as allowing merge only if all discussions are resolved' do
+      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+      post api('/projects', user), project
+
+      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+    end
+
     context 'when a visibility level is restricted' do
       before do
         @project = attributes_for(:project, { public: true })
@@ -332,13 +424,14 @@ describe API::API, api: true  do
         description: FFaker::Lorem.sentence,
         issues_enabled: false,
         merge_requests_enabled: false,
-        wiki_enabled: false
+        wiki_enabled: false,
+        request_access_enabled: true
       })
 
       post api("/projects/user/#{user.id}", admin), project
 
       project.each_pair do |k, v|
-        next if k == :path
+        next if %i[has_external_issue_tracker path].include?(k)
         expect(json_response[k.to_s]).to eq(v)
       end
     end
@@ -384,6 +477,34 @@ describe API::API, api: true  do
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 })
+      post api("/projects/user/#{user.id}", admin), project
+      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
+      project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+      post api("/projects/user/#{user.id}", admin), project
+      expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+    end
+
+    it 'sets a project as allowing merge even if discussions are unresolved' do
+      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+      post api("/projects/user/#{user.id}", admin), project
+
+      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+    end
+
+    it 'sets a project as allowing merge only if all discussions are resolved' do
+      project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+      post api("/projects/user/#{user.id}", admin), project
+
+      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+    end
   end
 
   describe "POST /projects/:id/uploads" do
@@ -444,6 +565,8 @@ describe API::API, 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_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
     end
 
     it 'returns a project by path name' do
@@ -523,37 +646,39 @@ describe API::API, api: true  do
       before do
         note = create(:note_on_issue, note: 'What an awesome day!', project: project)
         EventCreateService.new.leave_note(note, note.author)
-        get api("/projects/#{project.id}/events", user)
       end
 
-      it { expect(response).to have_http_status(200) }
+      it 'returns all events' do
+        get api("/projects/#{project.id}/events", user)
 
-      context 'joined event' do
-        let(:json_event) { json_response[1] }
+        expect(response).to have_http_status(200)
 
-        it { expect(json_event['action_name']).to eq('joined') }
-        it { expect(json_event['project_id'].to_i).to eq(project.id) }
-        it { expect(json_event['author_username']).to eq(user3.username) }
-        it { expect(json_event['author']['name']).to eq(user3.name) }
-      end
+        first_event = json_response.first
 
-      context 'comment event' do
-        let(:json_event) { json_response.first }
+        expect(first_event['action_name']).to eq('commented on')
+        expect(first_event['note']['body']).to eq('What an awesome day!')
 
-        it { expect(json_event['action_name']).to eq('commented on') }
-        it { expect(json_event['note']['body']).to eq('What an awesome day!') }
+        last_event = json_response.last
+
+        expect(last_event['action_name']).to eq('joined')
+        expect(last_event['project_id'].to_i).to eq(project.id)
+        expect(last_event['author_username']).to eq(user3.username)
+        expect(last_event['author']['name']).to eq(user3.name)
       end
     end
 
     it 'returns a 404 error if not found' do
       get api('/projects/42/events', user)
+
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Project Not Found')
     end
 
     it 'returns a 404 error if user is not a member' do
       other_user = create(:user)
+
       get api("/projects/#{project.id}/events", other_user)
+
       expect(response).to have_http_status(404)
     end
   end
@@ -726,13 +851,16 @@ describe API::API, api: true  do
     let(:group) { create(:group) }
 
     it "shares project with group" do
+      expires_at = 10.days.from_now.to_date
+
       expect do
-        post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+        post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
       end.to change { ProjectGroupLink.count }.by(1)
 
       expect(response.status).to eq 201
-      expect(json_response['group_id']).to eq group.id
-      expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+      expect(json_response['group_id']).to eq(group.id)
+      expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+      expect(json_response['expires_at']).to eq(expires_at.to_s)
     end
 
     it "returns a 400 error when group id is not given" do
@@ -751,6 +879,20 @@ describe API::API, api: true  do
       expect(response.status).to eq 400
     end
 
+    it 'returns a 404 error when user cannot read group' do
+      private_group = create(:group, :private)
+
+      post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+      expect(response.status).to eq 404
+    end
+
+    it 'returns a 404 error when group does not exist' do
+      post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+      expect(response.status).to eq 404
+    end
+
     it "returns a 409 error when wrong params passed" do
       post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
       expect(response.status).to eq 409
@@ -854,6 +996,15 @@ describe API::API, api: true  do
         expect(json_response['message']['name']).to eq(['has already been taken'])
       end
 
+      it 'updates request_access_enabled' do
+        project_param = { request_access_enabled: false }
+
+        put api("/projects/#{project.id}", user), project_param
+
+        expect(response).to have_http_status(200)
+        expect(json_response['request_access_enabled']).to eq(false)
+      end
+
       it 'updates path & name to existing path & name in different namespace' do
         project_param = { path: project4.path, name: project4.name }
         put api("/projects/#{project3.id}", user), project_param
@@ -915,7 +1066,8 @@ describe API::API, api: true  do
                           wiki_enabled: true,
                           snippets_enabled: true,
                           merge_requests_enabled: true,
-                          description: 'new description' }
+                          description: 'new description',
+                          request_access_enabled: true }
         put api("/projects/#{project.id}", user3), project_param
         expect(response).to have_http_status(403)
       end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 80a856a6e9095dcd4bc771e72b6b594dfb22232d..c4dc2d9006af22f88e63cfabc80a0bd158a13220 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -21,7 +21,7 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
 
         expect(json_response).to be_an Array
-        expect(json_response.first['name']).to eq('encoding')
+        expect(json_response.first['name']).to eq('bar')
         expect(json_response.first['type']).to eq('tree')
         expect(json_response.first['mode']).to eq('040000')
       end
@@ -166,9 +166,9 @@ describe API::API, api: true  do
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       contributor = json_response.first
-      expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com')
-      expect(contributor['name']).to eq('Dmitriy Zaporozhets')
-      expect(contributor['commits']).to eq(13)
+      expect(contributor['email']).to eq('tiagonbotelho@hotmail.com')
+      expect(contributor['name']).to eq('tiagonbotelho')
+      expect(contributor['commits']).to eq(1)
       expect(contributor['additions']).to eq(0)
       expect(contributor['deletions']).to eq(0)
     end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index f46f016135ee0b7b815e486ba0c39057c9024ef3..99414270be6c5d6eefefbc6cfd747cc19f39e230 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -226,7 +226,7 @@ describe API::Runners, api: true  do
     context 'authorized user' do
       context 'when runner is shared' do
         it 'does not update runner' do
-          put api("/runners/#{shared_runner.id}", user)
+          put api("/runners/#{shared_runner.id}", user), description: 'test'
 
           expect(response).to have_http_status(403)
         end
@@ -234,7 +234,7 @@ describe API::Runners, api: true  do
 
       context 'when runner is not shared' do
         it 'does not update runner without access to it' do
-          put api("/runners/#{specific_runner.id}", user2)
+          put api("/runners/#{specific_runner.id}", user2), description: 'test'
 
           expect(response).to have_http_status(403)
         end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 519e7ce12ad18760247d0b135bec73e7e11c29a2..e3f22b4c5788df516d33b1786fda2d9f5352ac19 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -17,6 +17,17 @@ describe API::API, api: true  do
         expect(json_response['can_create_project']).to eq(user.can_create_project?)
         expect(json_response['can_create_group']).to eq(user.can_create_group?)
       end
+
+      context 'with 2FA enabled' do
+        it 'rejects sign in attempts' do
+          user = create(:user, :two_factor)
+
+          post api('/session'), email: user.email, password: user.password
+
+          expect(response).to have_http_status(401)
+          expect(response.body).to include('You have 2FA enabled.')
+        end
+      end
     end
 
     context 'when email has case-typo and password is valid' do
@@ -56,22 +67,24 @@ describe API::API, api: true  do
     end
 
     context "when empty password" do
-      it "returns authentication error" do
+      it "returns authentication error with email" do
         post api("/session"), email: user.email
-        expect(response).to have_http_status(401)
 
-        expect(json_response['email']).to be_nil
-        expect(json_response['private_token']).to be_nil
+        expect(response).to have_http_status(400)
+      end
+
+      it "returns authentication error with username" do
+        post api("/session"), email: user.username
+
+        expect(response).to have_http_status(400)
       end
     end
 
     context "when empty name" do
       it "returns authentication error" do
         post api("/session"), password: user.password
-        expect(response).to have_http_status(401)
 
-        expect(json_response['email']).to be_nil
-        expect(json_response['private_token']).to be_nil
+        expect(response).to have_http_status(400)
       end
     end
   end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 54d096e8b7ffd9d66dc2d8a4301c421af8647882..096a8ebab706fe5a20d1214793001226699c5e57 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -14,22 +14,39 @@ describe API::API, 'Settings', api: true  do
       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
     end
   end
 
   describe "PUT /application/settings" do
-    before do
-      storages = { 'custom' => 'tmp/tests/custom_repositories' }
-      allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+    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 api("/application/settings", admin),
+          default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.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')
+      end
     end
 
-    it "updates application settings" do
-      put api("/application/settings", admin),
-        default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
-      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')
+    context "missing koding_url value when koding_enabled is true" do
+      it "returns a blank parameter error message" do
+        put api("/application/settings", admin), koding_enabled: true
+
+        expect(response).to have_http_status(400)
+        expect(json_response['message']).to have_key('koding_url')
+        expect(json_response['message']['koding_url']).to include "can't be blank"
+      end
     end
   end
 end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 1ce2658569eabcf0a8b4746f747f63b531351a3f..6c9df21f5983843feda4ad2d08fbbbba0fcf7d80 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -13,6 +13,7 @@ describe API::API, api: true  do
     context "when no user" do
       it "returns authentication error" do
         get api("/hooks")
+
         expect(response).to have_http_status(401)
       end
     end
@@ -20,6 +21,7 @@ describe API::API, api: true  do
     context "when not an admin" do
       it "returns forbidden error" do
         get api("/hooks", user)
+
         expect(response).to have_http_status(403)
       end
     end
@@ -27,9 +29,12 @@ describe API::API, api: true  do
     context "when authenticated as admin" do
       it "returns an array of hooks" do
         get api("/hooks", admin)
+
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.first['url']).to eq(hook.url)
+        expect(json_response.first['push_events']).to be true
+        expect(json_response.first['tag_push_events']).to be false
       end
     end
   end
@@ -43,6 +48,13 @@ describe API::API, api: true  do
 
     it "responds with 400 if url not given" do
       post api("/hooks", admin)
+
+      expect(response).to have_http_status(400)
+    end
+
+    it "responds with 400 if url is invalid" do
+      post api("/hooks", admin), url: 'hp://mep.mep'
+
       expect(response).to have_http_status(400)
     end
 
@@ -51,6 +63,14 @@ describe API::API, api: true  do
         post api("/hooks", admin)
       end.not_to change { SystemHook.count }
     end
+
+    it 'sets default values for events' do
+      post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true
+
+      expect(response).to have_http_status(201)
+      expect(json_response['enable_ssl_verification']).to be true
+      expect(json_response['tag_push_events']).to be false
+    end
   end
 
   describe "GET /hooks/:id" do
@@ -73,9 +93,10 @@ describe API::API, api: true  do
       end.to change { SystemHook.count }.by(-1)
     end
 
-    it "returns success if hook id not found" do
-      delete api("/hooks/12345", admin)
-      expect(response).to have_http_status(200)
+    it 'returns 404 if the system hook does not exist' do
+      delete api('/hooks/12345', admin)
+
+      expect(response).to have_http_status(404)
     end
   end
 end
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 5bd5b861792da3b92f187bbcf4688547d4e17d95..d32ba60fc4ca75c71b44101fcf9bd20d5f8b8d57 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,53 +3,201 @@ require 'spec_helper'
 describe API::Templates, api: true  do
   include ApiHelpers
 
-  context 'global templates' do
-    describe 'the Template Entity' do
-      before { get api('/gitignores/Ruby') }
+  shared_examples_for 'the Template Entity' do |path|
+    before { get api(path) }
 
-      it { expect(json_response['name']).to eq('Ruby') }
-      it { expect(json_response['content']).to include('*.gem') }
+    it { expect(json_response['name']).to eq('Ruby') }
+    it { expect(json_response['content']).to include('*.gem') }
+  end
+  
+  shared_examples_for 'the TemplateList Entity' do |path|
+    before { get api(path) }
+
+    it { expect(json_response.first['name']).not_to be_nil }
+    it { expect(json_response.first['content']).to be_nil }
+  end
+
+  shared_examples_for 'requesting gitignores' do |path|
+    it 'returns a list of available gitignore templates' do
+      get api(path)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.size).to be > 15
     end
+  end
 
-    describe 'the TemplateList Entity' do
-      before { get api('/gitignores') }
+  shared_examples_for 'requesting gitlab-ci-ymls' do |path|
+    it 'returns a list of available gitlab_ci_ymls' do
+      get api(path)
 
-      it { expect(json_response.first['name']).not_to be_nil }
-      it { expect(json_response.first['content']).to be_nil }
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.first['name']).not_to be_nil
     end
+  end
+
+  shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
+    it 'adds a disclaimer on the top' do
+      get api(path)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['content']).to start_with("# This file is a template,")
+    end
+  end
+
+  shared_examples_for 'the License Template Entity' do |path|
+    before { get api(path) }
+
+    it 'returns a license template' do
+      expect(json_response['key']).to eq('mit')
+      expect(json_response['name']).to eq('MIT License')
+      expect(json_response['nickname']).to be_nil
+      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['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)')
+    end
+  end
 
-    context 'requesting gitignores' do
-      describe 'GET /gitignores' do
-        it 'returns a list of available gitignore templates' do
-          get api('/gitignores')
+  shared_examples_for 'GET licenses' do |path|
+    it 'returns a list of available license templates' do
+      get api(path)
 
-          expect(response.status).to eq(200)
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(15)
+      expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+    end
+
+    describe 'the popular parameter' do
+      context 'with popular=1' do
+        it 'returns a list of available popular license templates' do
+          get api("#{path}?popular=1")
+
+          expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
-          expect(json_response.size).to be > 15
+          expect(json_response.size).to eq(3)
+          expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
         end
       end
     end
+  end
 
-    context 'requesting gitlab-ci-ymls' do
-      describe 'GET /gitlab_ci_ymls' do
-        it 'returns a list of available gitlab_ci_ymls' do
-          get api('/gitlab_ci_ymls')
+  shared_examples_for 'GET licenses/:name' do |path|
+    context 'with :project and :fullname given' do
+      before do
+        get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+      end
 
-          expect(response.status).to eq(200)
-          expect(json_response).to be_an Array
-          expect(json_response.first['name']).not_to be_nil
+      context 'for the mit license' do
+        let(:license_type) { 'mit' }
+
+        it 'returns the license text' do
+          expect(json_response['content']).to include('The MIT License (MIT)')
+        end
+
+        it 'replaces placeholder values' do
+          expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
+        end
+      end
+
+      context 'for the agpl-3.0 license' do
+        let(:license_type) { 'agpl-3.0' }
+
+        it 'returns the license text' do
+          expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+        end
+
+        it 'replaces placeholder values' do
+          expect(json_response['content']).to include('My Awesome Project')
+          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
+        end
+      end
+
+      context 'for the gpl-3.0 license' do
+        let(:license_type) { 'gpl-3.0' }
+
+        it 'returns the license text' do
+          expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+        end
+
+        it 'replaces placeholder values' do
+          expect(json_response['content']).to include('My Awesome Project')
+          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
+        end
+      end
+
+      context 'for the gpl-2.0 license' do
+        let(:license_type) { 'gpl-2.0' }
+
+        it 'returns the license text' do
+          expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+        end
+
+        it 'replaces placeholder values' do
+          expect(json_response['content']).to include('My Awesome Project')
+          expect(json_response['content']).to include("Copyright (C) #{Time.now.year}  Anton")
+        end
+      end
+
+      context 'for the apache-2.0 license' do
+        let(:license_type) { 'apache-2.0' }
+
+        it 'returns the license text' do
+          expect(json_response['content']).to include('Apache License')
+        end
+
+        it 'replaces placeholder values' do
+          expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
+        end
+      end
+
+      context 'for an uknown license' do
+        let(:license_type) { 'muth-over9000' }
+
+        it 'returns a 404' do
+          expect(response).to have_http_status(404)
         end
       end
     end
 
-    describe 'GET /gitlab_ci_ymls/Ruby' do
-      it 'adds a disclaimer on the top' do
-        get api('/gitlab_ci_ymls/Ruby')
+    context 'with no :fullname given' do
+      context 'with an authenticated user' do
+        let(:user) { create(:user) }
+
+        it 'replaces the copyright owner placeholder with the name of the current user' do
+          get api('/templates/licenses/mit', user)
 
-        expect(response).to have_http_status(200)
-        expect(json_response['name']).not_to be_nil
-        expect(json_response['content']).to start_with("# This file is a template,")
+          expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
+        end
       end
     end
   end
+
+  describe 'with /templates namespace' do
+    it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
+    it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
+    it_behaves_like 'requesting gitignores', '/templates/gitignores'
+    it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
+    it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
+    it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
+    it_behaves_like 'GET licenses', '/templates/licenses'
+    it_behaves_like 'GET licenses/:name', '/templates/licenses'
+  end
+
+  describe 'without /templates namespace' do
+    it_behaves_like 'the Template Entity', '/gitignores/Ruby'
+    it_behaves_like 'the TemplateList Entity', '/gitignores'
+    it_behaves_like 'requesting gitignores', '/gitignores'
+    it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
+    it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
+    it_behaves_like 'the License Template Entity', '/licenses/mit'
+    it_behaves_like 'GET licenses', '/licenses'
+    it_behaves_like 'GET licenses/:name', '/licenses'
+  end
 end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 82bba1ce8a40fd8bc94c13b235de6d64d8ff10da..8ba2eccf66c3c9e44034402b464646c7de3a9a1d 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -68,7 +68,7 @@ describe API::API do
         it 'validates variables to be a hash' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
           expect(response).to have_http_status(400)
-          expect(json_response['message']).to eq('variables needs to be a hash')
+          expect(json_response['error']).to eq('variables is invalid')
         end
 
         it 'validates variables needs to be a map of key-valued strings' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 0bbba64a6d581973ddadbfc91354c1e4ba585434..34d1f557e4bffab19460ae79b6a66c469a664a98 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -48,6 +48,17 @@ describe API::API, api: true  do
         end['username']).to eq(username)
       end
 
+      it "returns an array of blocked users" do
+        ldap_blocked_user
+        create(:user, state: 'blocked')
+
+        get api("/users?blocked=true", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
+      end
+
       it "returns one user" do
         get api("/users?username=#{omniauth_user.username}", user)
         expect(response).to have_http_status(200)
@@ -62,12 +73,23 @@ describe API::API, api: true  do
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.first.keys).to include 'email'
+        expect(json_response.first.keys).to include 'organization'
         expect(json_response.first.keys).to include 'identities'
         expect(json_response.first.keys).to include 'can_create_project'
         expect(json_response.first.keys).to include 'two_factor_enabled'
         expect(json_response.first.keys).to include 'last_sign_in_at'
         expect(json_response.first.keys).to include 'confirmed_at'
       end
+
+      it "returns an array of external users" do
+        create(:user, external: true)
+
+        get api("/users?external=true", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response).to all(include('external' => true))
+      end
     end
   end
 
@@ -89,8 +111,9 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "returns a 404 if invalid ID" do
+    it "returns a 404 for invalid ID" do
       get api("/users/1ASDF", user)
+
       expect(response).to have_http_status(404)
     end
   end
@@ -265,6 +288,14 @@ describe API::API, api: true  do
       expect(user.reload.bio).to eq('new test bio')
     end
 
+    it "updates user with organization" do
+      put api("/users/#{user.id}", admin), { organization: 'GitLab' }
+
+      expect(response).to have_http_status(200)
+      expect(json_response['organization']).to eq('GitLab')
+      expect(user.reload.organization).to eq('GitLab')
+    end
+
     it 'updates user with his own email' do
       put api("/users/#{user.id}", admin), email: user.email
       expect(response).to have_http_status(200)
@@ -331,8 +362,10 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "raises error for invalid ID" do
-      expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+    it "returns a 404 if invalid ID" do
+      put api("/users/ASDF", admin)
+
+      expect(response).to have_http_status(404)
     end
 
     it 'returns 400 error if user does not validate' do
@@ -484,8 +517,9 @@ describe API::API, api: true  do
       end.to change{ user.emails.count }.by(1)
     end
 
-    it "raises error for invalid ID" do
+    it "returns a 400 for invalid ID" do
       post api("/users/999999/emails", admin)
+
       expect(response).to have_http_status(400)
     end
   end
@@ -516,9 +550,10 @@ describe API::API, api: true  do
         expect(json_response.first['email']).to eq(email.email)
       end
 
-      it "raises error for invalid ID" do
+      it "returns a 404 for invalid ID" do
         put api("/users/ASDF/emails", admin)
-        expect(response).to have_http_status(405)
+
+        expect(response).to have_http_status(404)
       end
     end
   end
@@ -557,8 +592,10 @@ describe API::API, api: true  do
         expect(json_response['message']).to eq('404 Email Not Found')
       end
 
-      it "raises error for invalid ID" do
-        expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
+      it "returns a 404 for invalid ID" do
+        delete api("/users/ASDF/emails/bar", admin)
+
+        expect(response).to have_http_status(404)
       end
     end
   end
@@ -591,8 +628,10 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "raises error for invalid ID" do
-      expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+    it "returns a 404 for invalid ID" do
+      delete api("/users/ASDF", admin)
+
+      expect(response).to have_http_status(404)
     end
   end
 
@@ -605,6 +644,7 @@ describe API::API, api: true  do
       expect(json_response['can_create_project']).to eq(user.can_create_project?)
       expect(json_response['can_create_group']).to eq(user.can_create_group?)
       expect(json_response['projects_limit']).to eq(user.projects_limit)
+      expect(json_response['private_token']).to be_blank
     end
 
     it "returns 401 error if user is unauthenticated" do
@@ -644,6 +684,7 @@ describe API::API, api: true  do
 
     it "returns 404 Not Found within invalid ID" do
       get api("/user/keys/42", user)
+
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
@@ -659,6 +700,7 @@ describe API::API, api: true  do
 
     it "returns 404 for invalid ID" do
       get api("/users/keys/ASDF", admin)
+
       expect(response).to have_http_status(404)
     end
   end
@@ -717,8 +759,10 @@ describe API::API, api: true  do
       expect(response).to have_http_status(401)
     end
 
-    it "raises error for invalid ID" do
-      expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+    it "returns a 404 for invalid ID" do
+      delete api("/users/keys/ASDF", admin)
+
+      expect(response).to have_http_status(404)
     end
   end
 
@@ -768,6 +812,7 @@ describe API::API, api: true  do
 
     it "returns 404 for invalid ID" do
       get api("/users/emails/ASDF", admin)
+
       expect(response).to have_http_status(404)
     end
   end
@@ -815,12 +860,14 @@ describe API::API, api: true  do
       expect(response).to have_http_status(401)
     end
 
-    it "raises error for invalid ID" do
-      expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+    it "returns a 404 for invalid ID" do
+      delete api("/users/emails/ASDF", admin)
+
+      expect(response).to have_http_status(404)
     end
   end
 
-  describe 'PUT /user/:id/block' do
+  describe 'PUT /users/:id/block' do
     before { admin }
     it 'blocks existing user' do
       put api("/users/#{user.id}/block", admin)
@@ -847,7 +894,7 @@ describe API::API, api: true  do
     end
   end
 
-  describe 'PUT /user/:id/unblock' do
+  describe 'PUT /users/:id/unblock' do
     let(:blocked_user)  { create(:user, state: 'blocked') }
     before { admin }
 
@@ -881,8 +928,87 @@ describe API::API, api: true  do
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "raises error for invalid ID" do
-      expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
+    it "returns a 404 for invalid ID" do
+      put api("/users/ASDF/block", admin)
+
+      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
+      it_behaves_like 'a paginated resources' do
+        let(:request) { get api("/users/#{user.id}/events", user) }
+      end
+
+      context 'joined event' do
+        it 'returns the "joined" event' do
+          get api("/users/#{user.id}/events", user)
+
+          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 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 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/version_spec.rb b/spec/requests/api/version_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..54b69a0cae79edc0e21bff64704cf892e6eb0066
--- /dev/null
+++ b/spec/requests/api/version_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+  include ApiHelpers
+
+  describe 'GET /version' do
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        get api('/version')
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when authenticated' do
+      let(:user) { create(:user) }
+
+      it 'returns the version information' do
+        get api('/version', user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['version']).to eq(Gitlab::VERSION)
+        expect(json_response['revision']).to eq(Gitlab::REVISION)
+      end
+    end
+  end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index ca7932dc5da3d1ef2aa71232121ab8f84000367c..6d49c42c21519c1d33b92f4825d97326a0ddf16e 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -15,54 +15,73 @@ describe Ci::API::API do
 
     describe "POST /builds/register" do
       let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+      let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
 
-      it "starts a build" do
-        register_builds info: { platform: :darwin }
-
-        expect(response).to have_http_status(201)
-        expect(json_response['sha']).to eq(build.sha)
-        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" => "DB_NAME", "value" => "postgres", "public" => true }
-        )
+      shared_examples 'no builds available' do
+        context 'when runner sends version in User-Agent' do
+          context 'for stable version' do
+            it { expect(response).to have_http_status(204) }
+          end
+
+          context 'for beta version' do
+            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (1-5-stable; go1.6.3; linux/amd64)' }
+            it { expect(response).to have_http_status(204) }
+          end
+        end
+
+        context "when runner doesn't send version in User-Agent" do
+          let(:user_agent) { 'Go-http-client/1.1' }
+          it { expect(response).to have_http_status(404) }
+        end
+      end
+
+      context 'when there is a pending build' do
+        it 'starts a build' do
+          register_builds info: { platform: :darwin }
+
+          expect(response).to have_http_status(201)
+          expect(json_response['sha']).to eq(build.sha)
+          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" => "DB_NAME", "value" => "postgres", "public" => true }
+          )
+        end
+
+        it 'updates runner info' do
+          expect { register_builds }.to change { runner.reload.contacted_at }
+        end
       end
 
       context 'when builds are finished' do
         before do
           build.success
-        end
-
-        it "returns 404 error if no builds for specific runner" do
           register_builds
-
-          expect(response).to have_http_status(404)
         end
+
+        it_behaves_like 'no builds available'
       end
 
       context 'for other project with builds' do
         before do
           build.success
           create(:ci_build, :pending)
-        end
-
-        it "returns 404 error if no builds for shared runner" do
           register_builds
-
-          expect(response).to have_http_status(404)
         end
+
+        it_behaves_like 'no builds available'
       end
 
       context 'for shared runner' do
         let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
 
-        it "should return 404 error if no builds for shared runner" do
+        before do
           register_builds shared_runner.token
-
-          expect(response).to have_http_status(404)
         end
+
+        it_behaves_like 'no builds available'
       end
 
       context 'for triggered build' do
@@ -136,18 +155,32 @@ describe Ci::API::API do
         end
 
         context 'when runner is not allowed to pick untagged builds' do
-          before { runner.update_column(:run_untagged, false) }
-
-          it 'does not pick build' do
+          before do
+            runner.update_column(:run_untagged, false)
             register_builds
-
-            expect(response).to have_http_status 404
           end
+
+          it_behaves_like 'no builds available'
+        end
+      end
+
+      context 'when runner is paused' do
+        let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
+
+        it 'responds with 404' do
+          register_builds
+
+          expect(response).to have_http_status 404
+        end
+
+        it 'does not update runner info' do
+          expect { register_builds }
+            .not_to change { runner.reload.contacted_at }
         end
       end
 
       def register_builds(token = runner.token, **params)
-        post ci_api("/builds/register"), params.merge(token: token)
+        post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
       end
     end
 
@@ -187,26 +220,33 @@ describe Ci::API::API do
       end
 
       context 'when request is valid' do
-        it { expect(response.status).to eq 202 }
+        it 'gets correct response' do
+          expect(response.status).to eq 202
+          expect(response.header).to have_key 'Range'
+          expect(response.header).to have_key 'Build-Status'
+        end
+
         it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
-        it { expect(response.header).to have_key 'Range' }
-        it { expect(response.header).to have_key 'Build-Status' }
       end
 
       context 'when content-range start is too big' do
         let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
 
-        it { expect(response.status).to eq 416 }
-        it { expect(response.header).to have_key 'Range' }
-        it { expect(response.header['Range']).to eq '0-11' }
+        it 'gets correct response' 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 { expect(response.status).to eq 416 }
-        it { expect(response.header).to have_key 'Range' }
-        it { expect(response.header['Range']).to eq '0-11' }
+        it 'gets correct response' 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
@@ -230,8 +270,10 @@ describe Ci::API::API do
       let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
       let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
       let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
-      let(:headers) { { "GitLab-Workhorse" => "1.0" } }
-      let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
+      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(:token) { build.token }
+      let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
 
       before { build.run! }
 
@@ -239,27 +281,51 @@ describe Ci::API::API do
         context "should authorize posting artifact to running build" do
           it "using token as parameter" do
             post authorize_url, { token: build.token }, 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 "using token as header" do
             post authorize_url, {}, headers_with_token
+
             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 "using runners token" do
+            post authorize_url, { token: build.project.runners_token }, 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 "reject requests that did not go through gitlab-workhorse" do
+            headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+            post authorize_url, { token: build.token }, headers
+
+            expect(response).to have_http_status(500)
+          end
         end
 
         context "should fail to post too large artifact" do
           it "using token as parameter" do
             stub_application_setting(max_artifacts_size: 0)
+
             post authorize_url, { token: build.token, filesize: 100 }, headers
+
             expect(response).to have_http_status(413)
           end
 
           it "using token as header" do
             stub_application_setting(max_artifacts_size: 0)
+
             post authorize_url, { filesize: 100 }, headers_with_token
+
             expect(response).to have_http_status(413)
           end
         end
@@ -327,6 +393,16 @@ describe Ci::API::API do
 
               it_behaves_like 'successful artifacts upload'
             end
+
+            context 'when using runners token' do
+              let(:token) { build.project.runners_token }
+
+              before do
+                upload_artifacts(file_upload, headers_with_token)
+              end
+
+              it_behaves_like 'successful artifacts upload'
+            end
           end
 
           context 'posts artifacts file and metadata file' do
@@ -466,19 +542,40 @@ describe Ci::API::API do
 
         before do
           delete delete_url, token: build.token
-          build.reload
         end
 
-        it 'removes build artifacts' do
-          expect(response).to have_http_status(200)
-          expect(build.artifacts_file.exists?).to be_falsy
-          expect(build.artifacts_metadata.exists?).to be_falsy
-          expect(build.artifacts_size).to be_nil
+        shared_examples 'having removable artifacts' do
+          it 'removes build artifacts' do
+            build.reload
+
+            expect(response).to have_http_status(200)
+            expect(build.artifacts_file.exists?).to be_falsy
+            expect(build.artifacts_metadata.exists?).to be_falsy
+            expect(build.artifacts_size).to be_nil
+          end
+        end
+
+        context 'when using build token' do
+          before do
+            delete delete_url, token: build.token
+          end
+
+          it_behaves_like 'having removable artifacts'
+        end
+
+        context 'when using runnners token' do
+          before do
+            delete delete_url, token: build.project.runners_token
+          end
+
+          it_behaves_like 'having removable artifacts'
         end
       end
 
       describe 'GET /builds/:id/artifacts' do
-        before { get get_url, token: build.token }
+        before do
+          get get_url, token: token
+        end
 
         context 'build has artifacts' do
           let(:build) { create(:ci_build, :artifacts) }
@@ -487,13 +584,29 @@ describe Ci::API::API do
               'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
           end
 
-          it 'downloads artifact' do
-            expect(response).to have_http_status(200)
-            expect(response.headers).to include download_headers
+          shared_examples 'having downloadable artifacts' do
+            it 'download artifacts' do
+              expect(response).to have_http_status(200)
+              expect(response.headers).to include download_headers
+            end
+          end
+
+          context 'when using build token' do
+            let(:token) { build.token }
+
+            it_behaves_like 'having downloadable artifacts'
+          end
+
+          context 'when using runnners token' do
+            let(:token) { build.project.runners_token }
+
+            it_behaves_like 'having downloadable artifacts'
           end
         end
 
         context 'build does not has artifacts' do
+          let(:token) { build.token }
+
           it 'responds with not found' do
             expect(response).to have_http_status(404)
           end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index 43596f07cb5d138c4b6c7d81a8868fbb958c561b..d6c26fd8a94bb6f426b5d0ef9d512c7e244113d8 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -109,10 +109,12 @@ describe Ci::API::API do
   end
 
   describe "DELETE /runners/delete" do
-    let!(:runner) { FactoryGirl.create(:ci_runner) }
-    before { delete ci_api("/runners/delete"), token: runner.token }
+    it 'returns 200' do
+      runner = FactoryGirl.create(:ci_runner)
+      delete ci_api("/runners/delete"), token: runner.token
 
-    it { expect(response).to have_http_status 200 }
-    it { expect(Ci::Runner.count).to eq(0) }
+      expect(response).to have_http_status 200
+      expect(Ci::Runner.count).to eq(0)
+    end
   end
 end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 8537c252b589fbdf99bd23c3a27a5ef09865e96f..f1728d61def1be85ce85a015b154e094b79a8274 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,8 +1,8 @@
 require "spec_helper"
 
 describe 'Git HTTP requests', lib: true do
-  let(:user)    { create(:user) }
-  let(:project) { create(:project, path: 'project.git-project') }
+  include GitHttpHelpers
+  include WorkhorseHelpers
 
   it "gives WWW-Authenticate hints" do
     clone_get('doesnt/exist.git')
@@ -10,389 +10,540 @@ describe 'Git HTTP requests', lib: true do
     expect(response.header['WWW-Authenticate']).to start_with('Basic ')
   end
 
-  context "when the project doesn't exist" do
-    context "when no authentication is provided" do
-      it "responds with status 401 (no project existence information leak)" do
-        download('doesnt/exist.git') do |response|
-          expect(response).to have_http_status(401)
-        end
-      end
-    end
+  describe "User with no identities" do
+    let(:user)    { create(:user) }
+    let(:project) { create(:project, path: 'project.git-project') }
 
-    context "when username and password are provided" do
-      context "when authentication fails" do
-        it "responds with status 401" do
-          download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+    context "when the project doesn't exist" do
+      context "when no authentication is provided" do
+        it "responds with status 401 (no project existence information leak)" do
+          download('doesnt/exist.git') do |response|
             expect(response).to have_http_status(401)
           end
         end
       end
 
-      context "when authentication succeeds" do
-        it "responds with status 404" do
-          download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
-            expect(response).to have_http_status(404)
+      context "when username and password are provided" do
+        context "when authentication fails" do
+          it "responds with status 401" do
+            download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+              expect(response).to have_http_status(401)
+            end
           end
         end
-      end
-    end
-  end
-
-  context "when the Wiki for a project exists" do
-    it "responds with the right project" do
-      wiki = ProjectWiki.new(project)
-      project.update_attribute(:visibility_level, Project::PUBLIC)
-
-      download("/#{wiki.repository.path_with_namespace}.git") do |response|
-        json_body = ActiveSupport::JSON.decode(response.body)
 
-        expect(response).to have_http_status(200)
-        expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+        context "when authentication succeeds" do
+          it "responds with status 404" do
+            download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
       end
     end
-  end
-
-  context "when the project exists" do
-    let(:path) { "#{project.path_with_namespace}.git" }
 
-    context "when the project is public" do
-      before do
+    context "when the Wiki for a project exists" do
+      it "responds with the right project" do
+        wiki = ProjectWiki.new(project)
         project.update_attribute(:visibility_level, Project::PUBLIC)
-      end
 
-      it "downloads get status 200" do
-        download(path, {}) do |response|
+        download("/#{wiki.repository.path_with_namespace}.git") do |response|
+          json_body = ActiveSupport::JSON.decode(response.body)
+
           expect(response).to have_http_status(200)
+          expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+          expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
         end
       end
+    end
 
-      it "uploads get status 401" do
-        upload(path, {}) do |response|
-          expect(response).to have_http_status(401)
+    context "when the project exists" do
+      let(:path) { "#{project.path_with_namespace}.git" }
+
+      context "when the project is public" do
+        before do
+          project.update_attribute(:visibility_level, Project::PUBLIC)
         end
-      end
 
-      context "with correct credentials" do
-        let(:env) { { user: user.username, password: user.password } }
+        it "downloads get status 200" do
+          download(path, {}) do |response|
+            expect(response).to have_http_status(200)
+            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+          end
+        end
 
-        it "uploads get status 403" do
-          upload(path, env) do |response|
-            expect(response).to have_http_status(403)
+        it "uploads get status 401" do
+          upload(path, {}) do |response|
+            expect(response).to have_http_status(401)
           end
         end
 
-        context 'but git-receive-pack is disabled' do
-          it "responds with status 404" do
-            allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
+        context "with correct credentials" do
+          let(:env) { { user: user.username, password: user.password } }
 
+          it "uploads get status 403" do
             upload(path, env) do |response|
               expect(response).to have_http_status(403)
             end
           end
-        end
-      end
 
-      context 'but git-upload-pack is disabled' do
-        it "responds with status 404" do
-          allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
+          context 'but git-receive-pack is disabled' do
+            it "responds with status 404" do
+              allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
 
-          download(path, {}) do |response|
-            expect(response).to have_http_status(404)
+              upload(path, env) do |response|
+                expect(response).to have_http_status(403)
+              end
+            end
           end
         end
-      end
-    end
 
-    context "when the project is private" do
-      before do
-        project.update_attribute(:visibility_level, Project::PRIVATE)
-      end
+        context 'but git-upload-pack is disabled' do
+          it "responds with status 404" do
+            allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
 
-      context "when no authentication is provided" do
-        it "responds with status 401 to downloads" do
-          download(path, {}) do |response|
-            expect(response).to have_http_status(401)
+            download(path, {}) do |response|
+              expect(response).to have_http_status(404)
+            end
           end
         end
 
-        it "responds with status 401 to uploads" do
-          upload(path, {}) do |response|
-            expect(response).to have_http_status(401)
+        context 'when the request is not from gitlab-workhorse' do
+          it 'raises an exception' do
+            expect do
+              get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
+            end.to raise_error(JWT::DecodeError)
           end
         end
-      end
 
-      context "when username and password are provided" do
-        let(:env) { { user: user.username, password: 'nope' } }
+        context 'when the repo is public' do
+          context 'but the repo is disabled' do
+            it 'does not allow to clone the repo' do
+              project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)
 
-        context "when authentication fails" do
-          it "responds with status 401" do
-            download(path, env) do |response|
-              expect(response).to have_http_status(401)
+              download("#{project.path_with_namespace}.git", {}) do |response|
+                expect(response).to have_http_status(:unauthorized)
+              end
             end
           end
 
-          context "when the user is IP banned" do
-            it "responds with status 401" do
-              expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
-              allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
+          context 'but the repo is enabled' do
+            it 'allows to clone the repo' do
+              project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
 
-              clone_get(path, env)
+              download("#{project.path_with_namespace}.git", {}) do |response|
+                expect(response).to have_http_status(:ok)
+              end
+            end
+          end
 
-              expect(response).to have_http_status(401)
+          context 'but only project members are allowed' do
+            it 'does not allow to clone the repo' do
+              project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)
+
+              download("#{project.path_with_namespace}.git", {}) do |response|
+                expect(response).to have_http_status(:unauthorized)
+              end
             end
           end
         end
+      end
 
-        context "when authentication succeeds" do
-          let(:env) { { user: user.username, password: user.password } }
+      context "when the project is private" do
+        before do
+          project.update_attribute(:visibility_level, Project::PRIVATE)
+        end
 
-          context "when the user has access to the project" do
-            before do
-              project.team << [user, :master]
+        context "when no authentication is provided" do
+          it "responds with status 401 to downloads" do
+            download(path, {}) do |response|
+              expect(response).to have_http_status(401)
             end
+          end
 
-            context "when the user is blocked" do
-              it "responds with status 404" do
-                user.block
-                project.team << [user, :master]
+          it "responds with status 401 to uploads" do
+            upload(path, {}) do |response|
+              expect(response).to have_http_status(401)
+            end
+          end
+        end
 
-                download(path, env) do |response|
-                  expect(response).to have_http_status(404)
-                end
+        context "when username and password are provided" do
+          let(:env) { { user: user.username, password: 'nope' } }
+
+          context "when authentication fails" do
+            it "responds with status 401" do
+              download(path, env) do |response|
+                expect(response).to have_http_status(401)
               end
             end
 
-            context "when the user isn't blocked" do
-              it "downloads get status 200" do
-                expect(Rack::Attack::Allow2Ban).to receive(:reset)
+            context "when the user is IP banned" do
+              it "responds with status 401" do
+                expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+                allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
 
                 clone_get(path, env)
 
-                expect(response).to have_http_status(200)
-              end
-
-              it "uploads get status 200" do
-                upload(path, env) do |response|
-                  expect(response).to have_http_status(200)
-                end
+                expect(response).to have_http_status(401)
               end
             end
+          end
 
-            context "when an oauth token is provided" do
+          context "when authentication succeeds" do
+            let(:env) { { user: user.username, password: user.password } }
+
+            context "when the user has access to the project" do
               before do
-                application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
-                @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+                project.team << [user, :master]
               end
 
-              it "downloads get status 200" do
-                clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+              context "when the user is blocked" do
+                it "responds with status 404" do
+                  user.block
+                  project.team << [user, :master]
 
-                expect(response).to have_http_status(200)
+                  download(path, env) do |response|
+                    expect(response).to have_http_status(404)
+                  end
+                end
               end
 
-              it "uploads get status 401 (no project existence information leak)" do
-                push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+              context "when the user isn't blocked" do
+                it "downloads get status 200" do
+                  expect(Rack::Attack::Allow2Ban).to receive(:reset)
 
-                expect(response).to have_http_status(401)
+                  clone_get(path, env)
+
+                  expect(response).to have_http_status(200)
+                  expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+                end
+
+                it "uploads get status 200" do
+                  upload(path, env) do |response|
+                    expect(response).to have_http_status(200)
+                    expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+                  end
+                end
               end
-            end
 
-            context "when blank password attempts follow a valid login" do
-              def attempt_login(include_password)
-                password = include_password ? user.password : ""
-                clone_get path, user: user.username, password: password
-                response.status
+              context "when an oauth token is provided" do
+                before do
+                  application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+                  @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+                end
+
+                it "downloads get status 200" do
+                  clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+                  expect(response).to have_http_status(200)
+                  expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+                end
+
+                it "uploads get status 401 (no project existence information leak)" do
+                  push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+                  expect(response).to have_http_status(401)
+                end
               end
 
-              it "repeated attempts followed by successful attempt" do
-                options = Gitlab.config.rack_attack.git_basic_auth
-                maxretry = options[:maxretry] - 1
-                ip = '1.2.3.4'
+              context 'when user has 2FA enabled' do
+                let(:user) { create(:user, :two_factor) }
+                let(:access_token) { create(:personal_access_token, user: user) }
 
-                allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
-                Rack::Attack::Allow2Ban.reset(ip, options)
+                before do
+                  project.team << [user, :master]
+                end
 
-                maxretry.times.each do
-                  expect(attempt_login(false)).to eq(401)
+                context 'when username and password are provided' do
+                  it 'rejects the clone attempt' do
+                    download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+                      expect(response).to have_http_status(401)
+                      expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+                    end
+                  end
+
+                  it 'rejects the push attempt' do
+                    upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+                      expect(response).to have_http_status(401)
+                      expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+                    end
+                  end
                 end
 
-                expect(attempt_login(true)).to eq(200)
-                expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+                context 'when username and personal access token are provided' do
+                  it 'allows clones' do
+                    download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+                      expect(response).to have_http_status(200)
+                    end
+                  end
+
+                  it 'allows pushes' do
+                    upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+                      expect(response).to have_http_status(200)
+                    end
+                  end
+                end
+              end
 
-                maxretry.times.each do
-                  expect(attempt_login(false)).to eq(401)
+              context "when blank password attempts follow a valid login" do
+                def attempt_login(include_password)
+                  password = include_password ? user.password : ""
+                  clone_get path, user: user.username, password: password
+                  response.status
                 end
 
-                Rack::Attack::Allow2Ban.reset(ip, options)
+                it "repeated attempts followed by successful attempt" do
+                  options = Gitlab.config.rack_attack.git_basic_auth
+                  maxretry = options[:maxretry] - 1
+                  ip = '1.2.3.4'
+
+                  allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+                  Rack::Attack::Allow2Ban.reset(ip, options)
+
+                  maxretry.times.each do
+                    expect(attempt_login(false)).to eq(401)
+                  end
+
+                  expect(attempt_login(true)).to eq(200)
+                  expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+
+                  maxretry.times.each do
+                    expect(attempt_login(false)).to eq(401)
+                  end
+
+                  Rack::Attack::Allow2Ban.reset(ip, options)
+                end
               end
             end
-          end
 
-          context "when the user doesn't have access to the project" do
-            it "downloads get status 404" do
-              download(path, user: user.username, password: user.password) do |response|
-                expect(response).to have_http_status(404)
+            context "when the user doesn't have access to the project" do
+              it "downloads get status 404" do
+                download(path, user: user.username, password: user.password) do |response|
+                  expect(response).to have_http_status(404)
+                end
               end
-            end
 
-            it "uploads get status 404" do
-              upload(path, user: user.username, password: user.password) do |response|
-                expect(response).to have_http_status(404)
+              it "uploads get status 404" do
+                upload(path, user: user.username, password: user.password) do |response|
+                  expect(response).to have_http_status(404)
+                end
               end
             end
           end
         end
-      end
 
-      context "when a gitlab ci token is provided" do
-        let(:token) { 123 }
-        let(:project) { FactoryGirl.create :empty_project }
+        context "when a gitlab ci token is provided" do
+          let(:build) { create(:ci_build, :running) }
+          let(:project) { build.project }
+          let(:other_project) { create(:empty_project) }
 
-        before do
-          project.update_attributes(runners_token: token, builds_enabled: true)
-        end
+          before do
+            project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
+          end
 
-        it "downloads get status 200" do
-          clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+          context 'when build created by system is authenticated' do
+            it "downloads get status 200" do
+              clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
 
-          expect(response).to have_http_status(200)
-        end
+              expect(response).to have_http_status(200)
+              expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+            end
+
+            it "uploads get status 401 (no project existence information leak)" do
+              push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+              expect(response).to have_http_status(401)
+            end
 
-        it "uploads get status 401 (no project existence information leak)" do
-          push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+            it "downloads from other project get status 404" do
+              clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
 
-          expect(response).to have_http_status(401)
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'and build created by' do
+            before do
+              build.update(user: user)
+              project.team << [user, :reporter]
+            end
+
+            shared_examples 'can download code only' do
+              it 'downloads get status 200' do
+                clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+                expect(response).to have_http_status(200)
+                expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+              end
+
+              it 'uploads get status 403' do
+                push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+                expect(response).to have_http_status(401)
+              end
+            end
+
+            context 'administrator' do
+              let(:user) { create(:admin) }
+
+              it_behaves_like 'can download code only'
+
+              it 'downloads from other project get status 403' do
+                clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+                expect(response).to have_http_status(403)
+              end
+            end
+
+            context 'regular user' do
+              let(:user) { create(:user) }
+
+              it_behaves_like 'can download code only'
+
+              it 'downloads from other project get status 404' do
+                clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+                expect(response).to have_http_status(404)
+              end
+            end
+          end
         end
       end
     end
-  end
 
-  context "when the project path doesn't end in .git" do
-    context "GET info/refs" do
-      let(:path) { "/#{project.path_with_namespace}/info/refs" }
+    context "when the project path doesn't end in .git" do
+      context "GET info/refs" do
+        let(:path) { "/#{project.path_with_namespace}/info/refs" }
 
-      context "when no params are added" do
-        before { get path }
+        context "when no params are added" do
+          before { get path }
 
-        it "redirects to the .git suffix version" do
-          expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+          it "redirects to the .git suffix version" do
+            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+          end
         end
-      end
 
-      context "when the upload-pack service is requested" do
-        let(:params) { { service: 'git-upload-pack' } }
-        before { get path, params }
+        context "when the upload-pack service is requested" do
+          let(:params) { { service: 'git-upload-pack' } }
+          before { get path, params }
 
-        it "redirects to the .git suffix version" do
-          expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+          it "redirects to the .git suffix version" do
+            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+          end
         end
-      end
 
-      context "when the receive-pack service is requested" do
-        let(:params) { { service: 'git-receive-pack' } }
-        before { get path, params }
+        context "when the receive-pack service is requested" do
+          let(:params) { { service: 'git-receive-pack' } }
+          before { get path, params }
 
-        it "redirects to the .git suffix version" do
-          expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+          it "redirects to the .git suffix version" do
+            expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+          end
         end
-      end
 
-      context "when the params are anything else" do
-        let(:params) { { service: 'git-implode-pack' } }
-        before { get path, params }
+        context "when the params are anything else" do
+          let(:params) { { service: 'git-implode-pack' } }
+          before { get path, params }
 
-        it "redirects to the sign-in page" do
-          expect(response).to redirect_to(new_user_session_path)
+          it "redirects to the sign-in page" do
+            expect(response).to redirect_to(new_user_session_path)
+          end
         end
       end
-    end
 
-    context "POST git-upload-pack" do
-      it "fails to find a route" do
-        expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+      context "POST git-upload-pack" do
+        it "fails to find a route" do
+          expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+        end
       end
-    end
 
-    context "POST git-receive-pack" do
-      it "failes to find a route" do
-        expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+      context "POST git-receive-pack" do
+        it "failes to find a route" do
+          expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+        end
       end
     end
-  end
 
-  context "retrieving an info/refs file" do
-    before { project.update_attribute(:visibility_level, Project::PUBLIC) }
+    context "retrieving an info/refs file" do
+      before { project.update_attribute(:visibility_level, Project::PUBLIC) }
 
-    context "when the file exists" do
-      before do
-        # Provide a dummy file in its place
-        allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
-        allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
-          Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
-        end
+      context "when the file exists" do
+        before do
+          # Provide a dummy file in its place
+          allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
+          allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
+            Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt')
+          end
 
-        get "/#{project.path_with_namespace}/blob/master/info/refs"
-      end
+          get "/#{project.path_with_namespace}/blob/master/info/refs"
+        end
 
-      it "returns the file" do
-        expect(response).to have_http_status(200)
+        it "returns the file" do
+          expect(response).to have_http_status(200)
+        end
       end
-    end
 
-    context "when the file does not exist" do
-      before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
+      context "when the file does not exist" do
+        before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
 
-      it "returns not found" do
-        expect(response).to have_http_status(404)
+        it "returns not found" do
+          expect(response).to have_http_status(404)
+        end
       end
     end
   end
 
-  def clone_get(project, options = {})
-    get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
-  end
-
-  def clone_post(project, options = {})
-    post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
-  end
-
-  def push_get(project, options = {})
-    get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
-  end
-
-  def push_post(project, options = {})
-    post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
-  end
+  describe "User with LDAP identity" do
+    let(:user) { create(:omniauth_user, extern_uid: dn) }
+    let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
 
-  def download(project, user: nil, password: nil, spnego_request_token: nil)
-    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+    before do
+      allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+      allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil)
+      allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
+    end
 
-    clone_get(*args)
-    yield response
+    context "when authentication fails" do
+      context "when no authentication is provided" do
+        it "responds with status 401" do
+          download('doesnt/exist.git') do |response|
+            expect(response).to have_http_status(401)
+          end
+        end
+      end
 
-    clone_post(*args)
-    yield response
-  end
+      context "when username and invalid password are provided" do
+        it "responds with status 401" do
+          download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+            expect(response).to have_http_status(401)
+          end
+        end
+      end
+    end
 
-  def upload(project, user: nil, password: nil, spnego_request_token: nil)
-    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+    context "when authentication succeeds" do
+      context "when the project doesn't exist" do
+        it "responds with status 404" do
+          download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
 
-    push_get(*args)
-    yield response
+      context "when the project exists" do
+        let(:project) { create(:project, path: 'project.git-project') }
 
-    push_post(*args)
-    yield response
-  end
+        before do
+          project.team << [user, :master]
+        end
 
-  def auth_env(user, password, spnego_request_token)
-    env = {}
-    if user && password
-      env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
-    elsif spnego_request_token
-      env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+        it "responds with status 200" do
+          clone_get(path, user: user.username, password: user.password) do |response|
+            expect(response).to have_http_status(200)
+          end
+        end
+      end
     end
-
-    env
   end
 end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index c6172b9cc7d857fde28d4f9d0ff0f1a4337e875c..a3e7844b2f3a1e7ce22cb827c08ee31c90d4fc95 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -20,35 +20,56 @@ describe JwtController do
     end
   end
 
-  context 'when using authorized request' do
+  context 'when using authenticated request' do
     context 'using CI token' do
-      let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) }
-      let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
-
-      subject! { get '/jwt/auth', parameters, headers }
+      let(:build) { create(:ci_build, :running) }
+      let(:project) { build.project }
+      let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
 
       context 'project with enabled CI' do
-        let(:builds_enabled) { true }
+        subject! { get '/jwt/auth', parameters, headers }
 
         it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
       end
 
       context 'project with disabled CI' do
-        let(:builds_enabled) { false }
+        before do
+          project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+        end
 
-        it { expect(response).to have_http_status(403) }
+        subject! { get '/jwt/auth', parameters, headers }
+
+        it { expect(response).to have_http_status(401) }
       end
     end
 
     context 'using User login' do
       let(:user) { create(:user) }
-      let(:headers) { { authorization: credentials('user', 'password') } }
-
-      before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
+      let(:headers) { { authorization: credentials(user.username, user.password) } }
 
       subject! { get '/jwt/auth', parameters, headers }
 
       it { expect(service_class).to have_received(:new).with(nil, user, parameters) }
+
+      context 'when user has 2FA enabled' do
+        let(:user) { create(:user, :two_factor) }
+
+        context 'without personal token' do
+          it 'rejects the authorization attempt' do
+            expect(response).to have_http_status(401)
+            expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+          end
+        end
+
+        context 'with personal token' do
+          let(:access_token) { create(:personal_access_token, user: user) }
+          let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+
+          it 'accepts the authorization attempt' do
+            expect(response).to have_http_status(200)
+          end
+        end
+      end
     end
 
     context 'using invalid login' do
@@ -56,7 +77,21 @@ describe JwtController do
 
       subject! { get '/jwt/auth', parameters, headers }
 
-      it { expect(response).to have_http_status(403) }
+      it { expect(response).to have_http_status(401) }
+    end
+  end
+
+  context 'when using unauthenticated request' do
+    it 'accepts the authorization attempt' do
+      get '/jwt/auth', parameters
+
+      expect(response).to have_http_status(200)
+    end
+
+    it 'allows read access' do
+      expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
+
+      get '/jwt/auth', parameters
     end
   end
 
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 4c9b4a8ba422ef6f921b5130557ca82628bcdec2..9bfc84c7425186c472ac76cf9886bf428e05d20f 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe 'Git LFS API and storage' do
+  include WorkhorseHelpers
+
   let(:user) { create(:user) }
   let!(:lfs_object) { create(:lfs_object, :with_file) }
 
@@ -12,6 +14,7 @@ describe 'Git LFS API and storage' do
   end
   let(:authorization) { }
   let(:sendfile) { }
+  let(:pipeline) { create(:ci_empty_pipeline, project: project) }
 
   let(:sample_oid) { lfs_object.oid }
   let(:sample_size) { lfs_object.size }
@@ -44,6 +47,113 @@ describe 'Git LFS API and storage' do
     end
   end
 
+  context 'project specific LFS settings' do
+    let(:project) { create(:empty_project) }
+    let(:body) do
+      {
+        'objects' => [
+          { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+            'size' => 1575078
+          },
+          { 'oid' => sample_oid,
+            'size' => sample_size
+          }
+        ],
+        'operation' => 'upload'
+      }
+    end
+    let(:authorization) { authorize_user }
+
+    context 'with LFS disabled globally' do
+      before do
+        project.team << [user, :master]
+        allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+      end
+
+      describe 'LFS disabled in project' do
+        before do
+          project.update_attribute(:lfs_enabled, false)
+        end
+
+        it 'responds with a 501 message on upload' do
+          post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+          expect(response).to have_http_status(501)
+        end
+
+        it 'responds with a 501 message on download' do
+          get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+          expect(response).to have_http_status(501)
+        end
+      end
+
+      describe 'LFS enabled in project' do
+        before do
+          project.update_attribute(:lfs_enabled, true)
+        end
+
+        it 'responds with a 501 message on upload' do
+          post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+          expect(response).to have_http_status(501)
+        end
+
+        it 'responds with a 501 message on download' do
+          get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+          expect(response).to have_http_status(501)
+        end
+      end
+    end
+
+    context 'with LFS enabled globally' do
+      before do
+        project.team << [user, :master]
+        enable_lfs
+      end
+
+      describe 'LFS disabled in project' do
+        before do
+          project.update_attribute(:lfs_enabled, false)
+        end
+
+        it 'responds with a 403 message on upload' do
+          post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+          expect(response).to have_http_status(403)
+          expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+        end
+
+        it 'responds with a 403 message on download' do
+          get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+          expect(response).to have_http_status(403)
+          expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+        end
+      end
+
+      describe 'LFS enabled in project' do
+        before do
+          project.update_attribute(:lfs_enabled, true)
+        end
+
+        it 'responds with a 200 message on upload' do
+          post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+          expect(response).to have_http_status(200)
+          expect(json_response['objects'].first['size']).to eq(1575078)
+        end
+
+        it 'responds with a 200 message on download' do
+          get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+          expect(response).to have_http_status(200)
+        end
+      end
+    end
+  end
+
   describe 'deprecated API' do
     let(:project) { create(:empty_project) }
 
@@ -135,15 +245,109 @@ describe 'Git LFS API and storage' do
           end
         end
 
-        context 'when CI is authorized' do
-          let(:authorization) { authorize_ci_project }
+        context 'when deploy key is authorized' do
+          let(:key) { create(:deploy_key) }
+          let(:authorization) { authorize_deploy_key }
 
           let(:update_permissions) do
+            project.deploy_keys << key
             project.lfs_objects << lfs_object
           end
 
           it_behaves_like 'responds with a file'
         end
+
+        describe 'when using a user key' do
+          let(:authorization) { authorize_user_key }
+
+          context 'when user allowed' do
+            let(:update_permissions) do
+              project.team << [user, :master]
+              project.lfs_objects << lfs_object
+            end
+
+            it_behaves_like 'responds with a file'
+          end
+
+          context 'when user not allowed' do
+            let(:update_permissions) do
+              project.lfs_objects << lfs_object
+            end
+
+            it 'responds with status 404' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
+
+        context 'when build is authorized as' do
+          let(:authorization) { authorize_ci_project }
+
+          shared_examples 'can download LFS only from own projects' do
+            context 'for owned project' do
+              let(:project) { create(:empty_project, namespace: user.namespace) }
+
+              let(:update_permissions) do
+                project.lfs_objects << lfs_object
+              end
+
+              it_behaves_like 'responds with a file'
+            end
+
+            context 'for member of project' do
+              let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+              let(:update_permissions) do
+                project.team << [user, :reporter]
+                project.lfs_objects << lfs_object
+              end
+
+              it_behaves_like 'responds with a file'
+            end
+
+            context 'for other project' do
+              let(:other_project) { create(:empty_project) }
+              let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+              let(:update_permissions) do
+                project.lfs_objects << lfs_object
+              end
+
+              it 'rejects downloading code' do
+                expect(response).to have_http_status(other_project_status)
+              end
+            end
+          end
+
+          context 'administrator' do
+            let(:user) { create(:admin) }
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            it_behaves_like 'can download LFS only from own projects' do
+              # We render 403, because administrator does have normally access
+              let(:other_project_status) { 403 }
+            end
+          end
+
+          context 'regular user' do
+            let(:user) { create(:user) }
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            it_behaves_like 'can download LFS only from own projects' do
+              # We render 404, to prevent data leakage about existence of the project
+              let(:other_project_status) { 404 }
+            end
+          end
+
+          context 'does not have user' do
+            let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+            it_behaves_like 'can download LFS only from own projects' do
+              # We render 404, to prevent data leakage about existence of the project
+              let(:other_project_status) { 404 }
+            end
+          end
+        end
       end
 
       context 'without required headers' do
@@ -322,10 +526,62 @@ describe 'Git LFS API and storage' do
         end
       end
 
-      context 'when CI is authorized' do
+      context 'when build is authorized as' do
         let(:authorization) { authorize_ci_project }
 
-        it_behaves_like 'an authorized requests'
+        let(:update_lfs_permissions) do
+          project.lfs_objects << lfs_object
+        end
+
+        shared_examples 'can download LFS only from own projects' do
+          context 'for own project' do
+            let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+            let(:update_user_permissions) do
+              project.team << [user, :reporter]
+            end
+
+            it_behaves_like 'an authorized requests'
+          end
+
+          context 'for other project' do
+            let(:other_project) { create(:empty_project) }
+            let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+            it 'rejects downloading code' do
+              expect(response).to have_http_status(other_project_status)
+            end
+          end
+        end
+
+        context 'administrator' do
+          let(:user) { create(:admin) }
+          let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+          it_behaves_like 'can download LFS only from own projects' do
+            # We render 403, because administrator does have normally access
+            let(:other_project_status) { 403 }
+          end
+        end
+
+        context 'regular user' do
+          let(:user) { create(:user) }
+          let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+          it_behaves_like 'can download LFS only from own projects' do
+            # We render 404, to prevent data leakage about existence of the project
+            let(:other_project_status) { 404 }
+          end
+        end
+
+        context 'does not have user' do
+          let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+          it_behaves_like 'can download LFS only from own projects' do
+            # We render 404, to prevent data leakage about existence of the project
+            let(:other_project_status) { 404 }
+          end
+        end
       end
 
       context 'when user is not authenticated' do
@@ -474,11 +730,37 @@ describe 'Git LFS API and storage' do
           end
         end
 
-        context 'when CI is authorized' do
+        context 'when build is authorized' do
           let(:authorization) { authorize_ci_project }
 
-          it 'responds with 401' do
-            expect(response).to have_http_status(401)
+          context 'build has an user' do
+            let(:user) { create(:user) }
+
+            context 'tries to push to own project' do
+              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+              it 'responds with 401' do
+                expect(response).to have_http_status(401)
+              end
+            end
+
+            context 'tries to push to other project' do
+              let(:other_project) { create(:empty_project) }
+              let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+              it 'responds with 401' do
+                expect(response).to have_http_status(401)
+              end
+            end
+          end
+
+          context 'does not have user' do
+            let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+            it 'responds with 401' do
+              expect(response).to have_http_status(401)
+            end
           end
         end
       end
@@ -500,14 +782,6 @@ describe 'Git LFS API and storage' do
           end
         end
       end
-
-      context 'when CI is authorized' do
-        let(:authorization) { authorize_ci_project }
-
-        it 'responds with status 403' do
-          expect(response).to have_http_status(401)
-        end
-      end
     end
 
     describe 'unsupported' do
@@ -608,6 +882,12 @@ describe 'Git LFS API and storage' do
             project.team << [user, :developer]
           end
 
+          context 'and the request bypassed workhorse' do
+            it 'raises an exception' do
+              expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
+            end
+          end
+
           context 'and request is sent by gitlab-workhorse to authorize the request' do
             before do
               put_authorize
@@ -617,6 +897,10 @@ describe 'Git LFS API and storage' do
               expect(response).to have_http_status(200)
             end
 
+            it 'uses the gitlab-workhorse content type' do
+              expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+            end
+
             it 'responds with status 200, location of lfs store and object details' do
               expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
               expect(json_response['LfsOid']).to eq(sample_oid)
@@ -660,10 +944,51 @@ describe 'Git LFS API and storage' do
         end
       end
 
-      context 'when CI is authenticated' do
+      context 'when build is authorized' do
         let(:authorization) { authorize_ci_project }
 
-        it_behaves_like 'unauthorized'
+        context 'build has an user' do
+          let(:user) { create(:user) }
+
+          context 'tries to push to own project' do
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            before do
+              project.team << [user, :developer]
+              put_authorize
+            end
+
+            it 'responds with 401' do
+              expect(response).to have_http_status(401)
+            end
+          end
+
+          context 'tries to push to other project' do
+            let(:other_project) { create(:empty_project) }
+            let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            before do
+              put_authorize
+            end
+
+            it 'responds with 401' do
+              expect(response).to have_http_status(401)
+            end
+          end
+        end
+
+        context 'does not have user' do
+          let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+          before do
+            put_authorize
+          end
+
+          it 'responds with 401' do
+            expect(response).to have_http_status(401)
+          end
+        end
       end
 
       context 'for unauthenticated' do
@@ -720,10 +1045,42 @@ describe 'Git LFS API and storage' do
         end
       end
 
-      context 'when CI is authenticated' do
+      context 'when build is authorized' do
         let(:authorization) { authorize_ci_project }
 
-        it_behaves_like 'unauthorized'
+        before do
+          put_authorize
+        end
+
+        context 'build has an user' do
+          let(:user) { create(:user) }
+
+          context 'tries to push to own project' do
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            it 'responds with 401' do
+              expect(response).to have_http_status(401)
+            end
+          end
+
+          context 'tries to push to other project' do
+            let(:other_project) { create(:empty_project) }
+            let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+            let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+            it 'responds with 401' do
+              expect(response).to have_http_status(401)
+            end
+          end
+        end
+
+        context 'does not have user' do
+          let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+          it 'responds with 401' do
+            expect(response).to have_http_status(401)
+          end
+        end
       end
 
       context 'for unauthenticated' do
@@ -756,8 +1113,11 @@ describe 'Git LFS API and storage' do
       end
     end
 
-    def put_authorize
-      put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
+    def put_authorize(verified: true)
+      authorize_headers = headers
+      authorize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+      put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
     end
 
     def put_finalize(lfs_tmp = lfs_tmp_file)
@@ -775,13 +1135,21 @@ describe 'Git LFS API and storage' do
   end
 
   def authorize_ci_project
-    ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token)
+    ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
   end
 
   def authorize_user
     ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
   end
 
+  def authorize_deploy_key
+    ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
+  end
+
+  def authorize_user_key
+    ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
+  end
+
   def fork_project(project, user, object = nil)
     allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
     Projects::ForkService.new(project, user, {}).execute
diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e02f0eacc9349a8ac9edcb2702c5db6966aa8fd4
--- /dev/null
+++ b/spec/requests/projects/artifacts_controller_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Projects::ArtifactsController do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  let(:pipeline) do
+    create(:ci_pipeline,
+            project: project,
+            sha: project.commit.sha,
+            ref: project.default_branch,
+            status: 'success')
+  end
+
+  let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+  describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do
+    before do
+      project.team << [user, :developer]
+
+      login_as(user)
+    end
+
+    def path_from_ref(
+      ref = pipeline.ref, job = build.name, path = 'browse')
+      latest_succeeded_namespace_project_artifacts_path(
+        project.namespace,
+        project,
+        [ref, path].join('/'),
+        job: job)
+    end
+
+    context 'cannot find the build' 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 path_from_ref('TAIL', build.name)
+        end
+
+        it_behaves_like 'not found'
+      end
+
+      context 'has no such build' do
+        before do
+          get path_from_ref(pipeline.ref, 'NOBUILD')
+        end
+
+        it_behaves_like 'not found'
+      end
+
+      context 'has no path' do
+        before do
+          get path_from_ref(pipeline.sha, build.name, '')
+        end
+
+        it_behaves_like 'not found'
+      end
+    end
+
+    context 'found the build and redirect' do
+      shared_examples 'redirect to the build' do
+        it 'redirects' do
+          path = browse_namespace_project_build_artifacts_path(
+            project.namespace,
+            project,
+            build)
+
+          expect(response).to redirect_to(path)
+        end
+      end
+
+      context 'with regular branch' do
+        before do
+          pipeline.update(ref: 'master',
+                          sha: project.commit('master').sha)
+
+          get path_from_ref('master')
+        end
+
+        it_behaves_like 'redirect to the build'
+      end
+
+      context 'with branch name containing slash' do
+        before do
+          pipeline.update(ref: 'improve/awesome',
+                          sha: project.commit('improve/awesome').sha)
+
+          get path_from_ref('improve/awesome')
+        end
+
+        it_behaves_like 'redirect to the build'
+      end
+
+      context 'with branch name and path containing slashes' do
+        before do
+          pipeline.update(ref: 'improve/awesome',
+                          sha: project.commit('improve/awesome').sha)
+
+          get path_from_ref('improve/awesome', build.name, 'file/README.md')
+        end
+
+        it 'redirects' do
+          path = file_namespace_project_build_artifacts_path(
+            project.namespace,
+            project,
+            build,
+            'README.md')
+
+          expect(response).to redirect_to(path)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 77842057a1044917fb1a61a1ef406ea8f044fe5a..2322430d2121feaeb568c3042cac0399172b8199 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -337,7 +337,7 @@ describe Projects::CommitsController, 'routing' do
   end
 
   it 'to #show' do
-    expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'atom')
+    expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
   end
 end
 
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1d4df9197f68824fc2aac271d1f76f9276b5b230..61dca5d5a62c4c64fbccfdf44ba38fd856240d4b 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -9,31 +9,33 @@ require 'spec_helper'
 # user_calendar_activities   GET    /u/:username/calendar_activities(.:format)
 describe UsersController, "routing" do
   it "to #show" do
-    expect(get("/u/User")).to route_to('users#show', username: 'User')
+    allow(User).to receive(:find_by).and_return(true)
+
+    expect(get("/User")).to route_to('users#show', username: 'User')
   end
 
   it "to #groups" do
-    expect(get("/u/User/groups")).to route_to('users#groups', username: 'User')
+    expect(get("/users/User/groups")).to route_to('users#groups', username: 'User')
   end
 
   it "to #projects" do
-    expect(get("/u/User/projects")).to route_to('users#projects', username: 'User')
+    expect(get("/users/User/projects")).to route_to('users#projects', username: 'User')
   end
 
   it "to #contributed" do
-    expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User')
+    expect(get("/users/User/contributed")).to route_to('users#contributed', username: 'User')
   end
 
   it "to #snippets" do
-    expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User')
+    expect(get("/users/User/snippets")).to route_to('users#snippets', username: 'User')
   end
 
   it "to #calendar" do
-    expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User')
+    expect(get("/users/User/calendar")).to route_to('users#calendar', username: 'User')
   end
 
   it "to #calendar_activities" do
-    expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
+    expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
   end
 end
 
@@ -107,21 +109,28 @@ describe HelpController, "routing" do
   end
 
   it 'to #show' do
-    path = '/help/markdown/markdown.md'
+    path = '/help/user/markdown.md'
     expect(get(path)).to route_to('help#show',
-                                  path: 'markdown/markdown',
+                                  path: 'user/markdown',
                                   format: 'md')
 
     path = '/help/workflow/protected_branches/protected_branches1.png'
     expect(get(path)).to route_to('help#show',
                                   path: 'workflow/protected_branches/protected_branches1',
                                   format: 'png')
-    
+
     path = '/help/ui'
     expect(get(path)).to route_to('help#ui')
   end
 end
 
+#                      koding GET    /koding(.:format)                      koding#index
+describe KodingController, "routing" do
+  it "to #index" do
+    expect(get("/koding")).to route_to('koding#index')
+  end
+end
+
 #             profile_account GET    /profile/account(.:format)             profile#account
 #             profile_history GET    /profile/history(.:format)             profile#history
 #            profile_password PUT    /profile/password(.:format)            profile#password_update
@@ -257,7 +266,15 @@ describe "Groups", "routing" do
   end
 
   it "also display group#show on the short path" do
-    expect(get('/1')).to route_to('namespaces#show', id: '1')
+    allow(Group).to receive(:find_by).and_return(true)
+
+    expect(get('/1')).to route_to('groups#show', id: '1')
+  end
+
+  it "also display group#show with dot in the path" do
+    allow(Group).to receive(:find_by).and_return(true)
+
+    expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot')
   end
 end
 
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2734f5bedca97ab1605cb1752fbdd4f6870db500
--- /dev/null
+++ b/spec/serializers/build_entity_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe BuildEntity do
+  let(:entity) do
+    described_class.new(build, request: double)
+  end
+
+  subject { entity.as_json }
+
+  context 'when build is a regular job' do
+    let(:build) { create(:ci_build) }
+
+    it 'contains url to build page and retry action' do
+      expect(subject).to include(:build_url, :retry_url)
+      expect(subject).not_to include(:play_url)
+    end
+
+    it 'does not contain sensitive information' do
+      expect(subject).not_to include(/token/)
+      expect(subject).not_to include(/variables/)
+    end
+  end
+
+  context 'when build is a manual action' do
+    let(:build) { create(:ci_build, :manual) }
+
+    it 'contains url to play action' do
+      expect(subject).to include(:play_url)
+    end
+  end
+end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..628e35c9a286a7b889753fadad4f773571d84635
--- /dev/null
+++ b/spec/serializers/commit_entity_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe CommitEntity do
+  let(:entity) do
+    described_class.new(commit, request: request)
+  end
+
+  let(:request) { double('request') }
+  let(:project) { create(:project) }
+  let(:commit) { project.commit }
+
+  subject { entity.as_json }
+
+  before do
+    allow(request).to receive(:project).and_return(project)
+  end
+
+  context 'when commit author is a user' do
+    before do
+      create(:user, email: commit.author_email)
+    end
+
+    it 'contains information about user' do
+      expect(subject.fetch(:author)).not_to be_nil
+    end
+  end
+
+  context 'when commit author is not a user' do
+    it 'does not contain author details' do
+      expect(subject.fetch(:author)).to be_nil
+    end
+  end
+
+  it 'contains commit URL' do
+    expect(subject).to include(:commit_url)
+  end
+
+  it 'needs to receive project in the request' do
+    expect(request).to receive(:project)
+      .and_return(project)
+
+    subject
+  end
+end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..51b6de915714b5a157874cc58a8be551e7eef46e
--- /dev/null
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DeploymentEntity do
+  let(:entity) do
+    described_class.new(deployment, request: double)
+  end
+
+  let(:deployment) { create(:deployment) }
+
+  subject { entity.as_json }
+
+  it 'exposes internal deployment id'  do
+    expect(subject).to include(:iid)
+  end
+
+  it 'exposes nested information about branch' do
+    expect(subject[:ref][:name]).to eq 'master'
+    expect(subject[:ref][:ref_url]).not_to be_empty
+  end
+end
diff --git a/spec/serializers/entity_request_spec.rb b/spec/serializers/entity_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86654adfd541c5a658d3cbda672ed69c3e870bf0
--- /dev/null
+++ b/spec/serializers/entity_request_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe EntityRequest do
+  subject do
+    described_class.new(user: 'user', project: 'some project')
+  end
+
+  describe 'methods created' do
+    it 'defines accessible attributes' do
+      expect(subject.user).to eq 'user'
+      expect(subject.project).to eq 'some project'
+    end
+
+    it 'raises error when attribute is not defined' do
+      expect { subject.some_method }.to raise_error NoMethodError
+    end
+  end
+end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4ca8c299147d3b414e67c773f0ebeff15c7f5563
--- /dev/null
+++ b/spec/serializers/environment_entity_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe EnvironmentEntity do
+  let(:entity) do
+    described_class.new(environment, request: double)
+  end
+
+  let(:environment) { create(:environment) }
+  subject { entity.as_json }
+
+  it 'exposes latest deployment' do
+    expect(subject).to include(:last_deployment)
+  end
+
+  it 'exposes core elements of environment' do
+    expect(subject).to include(:id, :name, :state, :environment_url)
+  end
+end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37bc086826cfc18caa34d5740383b9dc94b6664d
--- /dev/null
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe EnvironmentSerializer do
+  let(:serializer) do
+    described_class
+      .new(user: user, project: project)
+      .represent(resource)
+  end
+
+  let(:json) { serializer.as_json }
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  context 'when there is a single object provided' do
+    before do
+      create(:ci_build, :manual, name: 'manual1',
+                                 pipeline: deployable.pipeline)
+    end
+
+    let(:deployment) do
+      create(:deployment, deployable: deployable,
+                          user: user,
+                          project: project,
+                          sha: project.commit.id)
+    end
+
+    let(:deployable) { create(:ci_build) }
+    let(:resource) { deployment.environment }
+
+    it 'it generates payload for single object' do
+      expect(json).to be_an_instance_of Hash
+    end
+
+    it 'contains important elements of environment' do
+      expect(json)
+        .to include(:name, :external_url, :environment_url, :last_deployment)
+    end
+
+    it 'contains relevant information about last deployment' do
+      last_deployment = json.fetch(:last_deployment)
+
+      expect(last_deployment)
+        .to include(:ref, :user, :commit, :deployable, :manual_actions)
+    end
+  end
+
+  context 'when there is a collection of objects provided' do
+    let(:project) { create(:empty_project) }
+    let(:resource) { create_list(:environment, 2) }
+
+    it 'contains important elements of environment' do
+      expect(json.first)
+        .to include(:last_deployment, :name, :external_url)
+    end
+
+    it 'generates payload for collection' do
+      expect(json).to be_an_instance_of Array
+    end
+  end
+end
diff --git a/spec/serializers/user_entity_spec.rb b/spec/serializers/user_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c5d11cbcf5ee4b2f3842109fb154613901f27182
--- /dev/null
+++ b/spec/serializers/user_entity_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe UserEntity do
+  let(:entity) { described_class.new(user) }
+  let(:user) { create(:user) }
+  subject { entity.as_json }
+
+  it 'exposes user name and login' do
+    expect(subject).to include(:username, :name)
+  end
+
+  it 'does not expose passwords' do
+    expect(subject).not_to include(/password/)
+  end
+
+  it 'does not expose tokens' do
+    expect(subject).not_to include(/token/)
+  end
+
+  it 'does not expose 2FA OTPs' do
+    expect(subject).not_to include(/otp/)
+  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 7cc71f706ce6c9638b473408ece6f204d5335125..bb26513103d08b0efeddeeec95fdd804cc6788a0 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
   let(:current_params) { {} }
   let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
   let(:payload) { JWT.decode(subject[:token], rsa_key).first }
+  let(:authentication_abilities) do
+    [
+      :read_container_image,
+      :create_container_image
+    ]
+  end
 
-  subject { described_class.new(current_project, current_user, current_params).execute }
+  subject { described_class.new(current_project, current_user, current_params).execute(authentication_abilities: authentication_abilities) }
 
   before do
     allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
@@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
     end
   end
 
-  context 'project authorization' do
+  context 'build authorized as user' do
     let(:current_project) { create(:empty_project) }
+    let(:current_user) { create(:user) }
+    let(:authentication_abilities) do
+      [
+        :build_read_container_image,
+        :build_create_container_image
+      ]
+    end
 
-    context 'allow to use scope-less authentication' do
-      it_behaves_like 'a valid token'
+    before do
+      current_project.team << [current_user, :developer]
     end
 
+    it_behaves_like 'a valid token'
+
     context 'allow to pull and push images' do
       let(:current_params) do
         { scope: "repository:#{current_project.path_with_namespace}:pull,push" }
@@ -214,12 +229,56 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
 
         context 'allow for public' do
           let(:project) { create(:empty_project, :public) }
+
           it_behaves_like 'a pullable'
         end
 
-        context 'disallow for private' do
+        shared_examples 'pullable for being team member' do
+          context 'when you are not member' do
+            it_behaves_like 'an inaccessible'
+          end
+
+          context 'when you are member' do
+            before do
+              project.team << [current_user, :developer]
+            end
+
+            it_behaves_like 'a pullable'
+          end
+
+          context 'when you are owner' do
+            let(:project) { create(:empty_project, namespace: current_user.namespace) }
+
+            it_behaves_like 'a pullable'
+          end
+        end
+
+        context 'for private' do
           let(:project) { create(:empty_project, :private) }
-          it_behaves_like 'an inaccessible'
+
+          it_behaves_like 'pullable for being team member'
+
+          context 'when you are admin' do
+            let(:current_user) { create(:admin) }
+
+            context 'when you are not member' do
+              it_behaves_like 'an inaccessible'
+            end
+
+            context 'when you are member' do
+              before do
+                project.team << [current_user, :developer]
+              end
+
+              it_behaves_like 'a pullable'
+            end
+
+            context 'when you are owner' do
+              let(:project) { create(:empty_project, namespace: current_user.namespace) }
+
+              it_behaves_like 'a pullable'
+            end
+          end
         end
       end
 
@@ -229,8 +288,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
         end
 
         context 'disallow for all' do
-          let(:project) { create(:empty_project, :public) }
-          it_behaves_like 'an inaccessible'
+          context 'when you are member' do
+            let(:project) { create(:empty_project, :public) }
+
+            before do
+              project.team << [current_user, :developer]
+            end
+
+            it_behaves_like 'an inaccessible'
+          end
+
+          context 'when you are owner' do
+            let(:project) { create(:empty_project, :public, namespace: current_user.namespace) }
+
+            it_behaves_like 'an inaccessible'
+          end
         end
       end
     end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fde807cc4107f7377fed7ddaffc2711b3b8e7cad
--- /dev/null
+++ b/spec/services/boards/create_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Boards::CreateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+
+    subject(:service) { described_class.new(project, double) }
+
+    context 'when project does not have a board' do
+      it 'creates a new board' do
+        expect { service.execute }.to change(Board, :count).by(1)
+      end
+
+      it 'creates default lists' do
+        board = service.execute
+
+        expect(board.lists.size).to eq 2
+        expect(board.lists.first).to be_backlog
+        expect(board.lists.last).to be_done
+      end
+    end
+
+    context 'when project has a board' do
+      before do
+        create(:board, project: project)
+      end
+
+      it 'does not create a new board' do
+        expect { service.execute }.not_to change(project.boards, :count)
+      end
+    end
+  end
+end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..360ee398f77f7699e191d585384df6c5980180f5
--- /dev/null
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Boards::Issues::CreateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+    let(:user)    { create(:user) }
+    let(:label)   { create(:label, project: project, name: 'in-progress') }
+    let!(:list)   { create(:list, board: board, label: label, position: 0) }
+
+    subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+
+    before do
+      project.team << [user, :developer]
+    end
+
+    it 'delegates the create proceedings to Issues::CreateService' do
+      expect_any_instance_of(Issues::CreateService).to receive(:execute).once
+
+      service.execute
+    end
+
+    it 'creates a new issue' do
+      expect { service.execute }.to change(project.issues, :count).by(1)
+    end
+
+    it 'adds the label of the list to the issue' do
+      issue = service.execute
+
+      expect(issue.labels).to eq [label]
+    end
+  end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c206cf3ce7e70ec8a92939891455b3f1e2403ad
--- /dev/null
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Boards::Issues::ListService, services: true do
+  describe '#execute' do
+    let(:user)    { create(:user) }
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+
+    let(:bug) { create(:label, project: project, name: 'Bug') }
+    let(:development) { create(:label, project: project, name: 'Development') }
+    let(:testing)  { create(:label, project: project, name: 'Testing') }
+    let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+    let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+    let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
+
+    let!(:backlog) { create(:backlog_list, board: board) }
+    let!(:list1)   { create(:list, board: board, label: development, position: 0) }
+    let!(:list2)   { create(:list, board: board, label: testing, position: 1) }
+    let!(:done)    { create(:done_list, board: board) }
+
+    let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
+    let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+    let!(:reopened_issue1) { create(:issue, :reopened, project: project) }
+
+    let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) }
+    let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) }
+    let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) }
+    let!(:list2_issue1) { create(:labeled_issue, project: project, labels: [testing]) }
+
+    let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+    let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
+    let!(:closed_issue3) { create(:issue, :closed, project: project) }
+    let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
+
+    before do
+      project.team << [user, :developer]
+    end
+
+    it 'delegates search to IssuesFinder' do
+      params = { board_id: board.id, id: list1.id }
+
+      expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+      described_class.new(project, user, params).execute
+    end
+
+    context 'sets default order to priority' do
+      it 'returns opened issues when listing issues from Backlog' do
+        params = { board_id: board.id, id: backlog.id }
+
+        issues = described_class.new(project, user, params).execute
+
+        expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+      end
+
+      it 'returns closed issues when listing issues from Done' do
+        params = { board_id: board.id, id: done.id }
+
+        issues = described_class.new(project, user, params).execute
+
+        expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
+      end
+
+      it 'returns opened issues that have label list applied when listing issues from a label list' do
+        params = { board_id: board.id, id: list1.id }
+
+        issues = described_class.new(project, user, params).execute
+
+        expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+      end
+    end
+
+    context 'with list that does not belong to the board' do
+      it 'raises an error' do
+        list = create(:list)
+        service = described_class.new(project, user, board_id: board.id, id: list.id)
+
+        expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+
+    context 'with invalid list id' do
+      it 'raises an error' do
+        service = described_class.new(project, user, board_id: board.id, id: nil)
+
+        expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+  end
+end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c43b2aec490fb291222b89c27c05a8f34d901290
--- /dev/null
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Boards::Issues::MoveService, services: true do
+  describe '#execute' do
+    let(:user)    { create(:user) }
+    let(:project) { create(:empty_project) }
+    let(:board1)  { create(:board, project: project) }
+
+    let(:bug) { create(:label, project: project, name: 'Bug') }
+    let(:development) { create(:label, project: project, name: 'Development') }
+    let(:testing)  { create(:label, project: project, name: 'Testing') }
+
+    let!(:backlog) { create(:backlog_list, board: board1) }
+    let!(:list1)   { create(:list, board: board1, label: development, position: 0) }
+    let!(:list2)   { create(:list, board: board1, label: testing, position: 1) }
+    let!(:done)    { create(:done_list, board: board1) }
+
+    before do
+      project.team << [user, :developer]
+    end
+
+    context 'when moving from backlog' do
+      it 'adds the label of the list it goes to' do
+        issue = create(:labeled_issue, project: project, labels: [bug])
+        params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
+
+        described_class.new(project, user, params).execute(issue)
+
+        expect(issue.reload.labels).to contain_exactly(bug, development)
+      end
+    end
+
+    context 'when moving to backlog' do
+      it 'removes all list-labels' do
+        issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
+        params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
+
+        described_class.new(project, user, params).execute(issue)
+
+        expect(issue.reload.labels).to contain_exactly(bug)
+      end
+    end
+
+    context 'when moving from backlog to done' do
+      it 'closes the issue' do
+        issue = create(:labeled_issue, project: project, labels: [bug])
+        params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
+
+        described_class.new(project, user, params).execute(issue)
+        issue.reload
+
+        expect(issue.labels).to contain_exactly(bug)
+        expect(issue).to be_closed
+      end
+    end
+
+    context 'when moving an issue between lists' 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: list2.id } }
+
+      it 'delegates the label changes to Issues::UpdateService' do
+        expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
+
+        described_class.new(project, user, params).execute(issue)
+      end
+
+      it 'removes the label from the list it came from and adds the label of the list it goes to' do
+        described_class.new(project, user, params).execute(issue)
+
+        expect(issue.reload.labels).to contain_exactly(bug, testing)
+      end
+    end
+
+    context 'when moving to done' do
+      let(:board2) { create(:board, project: project) }
+      let(:regression) { create(:label, project: project, name: 'Regression') }
+      let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
+
+      let(:issue)  { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
+      let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: done.id } }
+
+      it 'delegates the close proceedings to Issues::CloseService' do
+        expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
+
+        described_class.new(project, user, params).execute(issue)
+      end
+
+      it 'removes all list-labels from project boards and close the issue' do
+        described_class.new(project, user, params).execute(issue)
+        issue.reload
+
+        expect(issue.labels).to contain_exactly(bug)
+        expect(issue).to be_closed
+      end
+    end
+
+    context 'when moving from done' do
+      let(:issue)  { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+      let(:params) { { board_id: board1.id, from_list_id: done.id, to_list_id: list2.id } }
+
+      it 'delegates the re-open proceedings to Issues::ReopenService' do
+        expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
+
+        described_class.new(project, user, params).execute(issue)
+      end
+
+      it 'adds the label of the list it goes to and reopen the issue' do
+        described_class.new(project, user, params).execute(issue)
+        issue.reload
+
+        expect(issue.labels).to contain_exactly(bug, testing)
+        expect(issue).to be_reopened
+      end
+    end
+
+    context 'when moving from done to backlog' do
+      it 'reopens the issue' do
+        issue = create(:labeled_issue, :closed, project: project, labels: [bug])
+        params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
+
+        described_class.new(project, user, params).execute(issue)
+        issue.reload
+
+        expect(issue.labels).to contain_exactly(bug)
+        expect(issue).to be_reopened
+      end
+    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 } }
+
+      it 'returns false' do
+        expect(described_class.new(project, user, params).execute(issue)).to eq false
+      end
+
+      it 'keeps issues labels' do
+        described_class.new(project, user, params).execute(issue)
+
+        expect(issue.reload.labels).to contain_exactly(bug, development)
+      end
+    end
+  end
+end
diff --git a/spec/services/boards/list_service_spec.rb b/spec/services/boards/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dff33e4bcbb8850d4fefe3140626ebd6cdac396d
--- /dev/null
+++ b/spec/services/boards/list_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Boards::ListService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+
+    subject(:service) { described_class.new(project, double) }
+
+    context 'when project does not have a board' do
+      it 'creates a new project board' do
+        expect { service.execute }.to change(project.boards, :count).by(1)
+      end
+
+      it 'delegates the project board creation to Boards::CreateService' do
+        expect_any_instance_of(Boards::CreateService).to receive(:execute).once
+
+        service.execute
+      end
+    end
+
+    context 'when project has a board' do
+      before do
+        create(:board, project: project)
+      end
+
+      it 'does not create a new board' do
+        expect { service.execute }.not_to change(project.boards, :count)
+      end
+    end
+
+    it 'returns project boards' do
+      board = create(:board, project: project)
+
+      expect(service.execute).to match_array [board]
+    end
+  end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7e9efcf93f41c2936d99b1c957708ce99d33482
--- /dev/null
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Boards::Lists::CreateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+    let(:user)    { create(:user) }
+    let(:label)   { create(:label, project: project, name: 'in-progress') }
+
+    subject(:service) { described_class.new(project, user, label_id: label.id) }
+
+    before do
+      project.team << [user, :developer]
+    end
+
+    context 'when board lists is empty' do
+      it 'creates a new list at beginning of the list' do
+        list = service.execute(board)
+
+        expect(list.position).to eq 0
+      end
+    end
+
+    context 'when board lists has backlog, and done lists' do
+      it 'creates a new list at beginning of the list' do
+        list = service.execute(board)
+
+        expect(list.position).to eq 0
+      end
+    end
+
+    context 'when board lists has labels lists' do
+      it 'creates a new list at end of the lists' do
+        create(:list, board: board, position: 0)
+        create(:list, board: board, position: 1)
+
+        list = service.execute(board)
+
+        expect(list.position).to eq 2
+      end
+    end
+
+    context 'when board lists has backlog, label and done lists' do
+      it 'creates a new list at end of the label lists' do
+        list1 = create(:list, board: board, position: 0)
+
+        list2 = service.execute(board)
+
+        expect(list1.reload.position).to eq 0
+        expect(list2.reload.position).to eq 1
+      end
+    end
+
+    context 'when provided label does not belongs to the project' do
+      it 'raises an error' do
+        label = create(:label, name: 'in-development')
+        service = described_class.new(project, user, label_id: label.id)
+
+        expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+  end
+end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..628caf034765745eff2591835e8f179ee6531295
--- /dev/null
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Boards::Lists::DestroyService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+    let(:user)    { create(:user) }
+
+    context 'when list type is label' do
+      it 'removes list from board' do
+        list = create(:list, board: board)
+        service = described_class.new(project, user)
+
+        expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+      end
+
+      it 'decrements position of higher lists' do
+        backlog     = board.backlog_list
+        development = create(:list, board: board, position: 0)
+        review      = create(:list, board: board, position: 1)
+        staging     = create(:list, board: board, position: 2)
+        done        = board.done_list
+
+        described_class.new(project, user).execute(development)
+
+        expect(backlog.reload.position).to be_nil
+        expect(review.reload.position).to eq 0
+        expect(staging.reload.position).to eq 1
+        expect(done.reload.position).to be_nil
+      end
+    end
+
+    it 'does not remove list from board when list type is backlog' do
+      list = board.backlog_list
+      service = described_class.new(project, user)
+
+      expect { service.execute(list) }.not_to change(board.lists, :count)
+    end
+
+    it 'does not remove list from board when list type is done' do
+      list = board.done_list
+      service = described_class.new(project, user)
+
+      expect { service.execute(list) }.not_to change(board.lists, :count)
+    end
+  end
+end
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed0337662af0dcf9fb80bf43442de5544e2fc7c4
--- /dev/null
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Boards::Lists::GenerateService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+    let(:user)    { create(:user) }
+
+    subject(:service) { described_class.new(project, user) }
+
+    before do
+      project.team << [user, :developer]
+    end
+
+    context 'when board lists is empty' do
+      it 'creates the default lists' do
+        expect { service.execute(board) }.to change(board.lists, :count).by(2)
+      end
+    end
+
+    context 'when board lists is not empty' do
+      it 'does not creates the default lists' do
+        create(:list, board: board)
+
+        expect { service.execute(board) }.not_to change(board.lists, :count)
+      end
+    end
+
+    context 'when project labels does not contains any list label' do
+      it 'creates labels' do
+        expect { service.execute(board) }.to change(project.labels, :count).by(2)
+      end
+    end
+
+    context 'when project labels contains some of list label' do
+      it 'creates the missing labels' do
+        create(:label, project: project, name: 'Doing')
+
+        expect { service.execute(board) }.to change(project.labels, :count).by(1)
+      end
+    end
+  end
+end
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..334cee3f06d364977ca6657dde52761a54529e2a
--- /dev/null
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Boards::Lists::ListService, services: true do
+  describe '#execute' do
+    it "returns board's lists" do
+      project = create(:empty_project)
+      board = create(:board, project: project)
+      label = create(:label, project: project)
+      list = create(:list, board: board, label: label)
+
+      service = described_class.new(project, double)
+
+      expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+    end
+  end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63fa0bb8c5f6e8225b3fc5990785dc7fc2f4769f
--- /dev/null
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Boards::Lists::MoveService, services: true do
+  describe '#execute' do
+    let(:project) { create(:empty_project) }
+    let(:board)   { create(:board, project: project) }
+    let(:user)    { create(:user) }
+
+    let!(:backlog)     { create(:backlog_list, board: board) }
+    let!(:planning)    { create(:list, board: board, position: 0) }
+    let!(:development) { create(:list, board: board, position: 1) }
+    let!(:review)      { create(:list, board: board, position: 2) }
+    let!(:staging)     { create(:list, board: board, position: 3) }
+    let!(:done)        { create(:done_list, board: board) }
+
+    context 'when list type is set to label' do
+      it 'keeps position of lists when new position is nil' do
+        service = described_class.new(project, user, position: nil)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [0, 1, 2, 3]
+      end
+
+      it 'keeps position of lists when new positon is equal to old position' do
+        service = described_class.new(project, user, position: planning.position)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [0, 1, 2, 3]
+      end
+
+      it 'keeps position of lists when new positon is negative' do
+        service = described_class.new(project, user, position: -1)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [0, 1, 2, 3]
+      end
+
+      it 'keeps position of lists when new positon is equal to number of labels lists' do
+        service = described_class.new(project, user, position: board.lists.label.size)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [0, 1, 2, 3]
+      end
+
+      it 'keeps position of lists when new positon is greater than number of labels lists' do
+        service = described_class.new(project, user, position: board.lists.label.size + 1)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [0, 1, 2, 3]
+      end
+
+      it 'increments position of intermediate lists when new positon is equal to first position' do
+        service = described_class.new(project, user, position: 0)
+
+        service.execute(staging)
+
+        expect(current_list_positions).to eq [1, 2, 3, 0]
+      end
+
+      it 'decrements position of intermediate lists when new positon is equal to last position' do
+        service = described_class.new(project, user, position: board.lists.label.last.position)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [3, 0, 1, 2]
+      end
+
+      it 'decrements position of intermediate lists when new position is greater than old position' do
+        service = described_class.new(project, user, position: 2)
+
+        service.execute(planning)
+
+        expect(current_list_positions).to eq [2, 0, 1, 3]
+      end
+
+      it 'increments position of intermediate lists when new position is lower than old position' do
+        service = described_class.new(project, user, position: 1)
+
+        service.execute(staging)
+
+        expect(current_list_positions).to eq [0, 2, 3, 1]
+      end
+    end
+
+    it 'keeps position of lists when list type is backlog' do
+      service = described_class.new(project, user, position: 2)
+
+      service.execute(backlog)
+
+      expect(current_list_positions).to eq [0, 1, 2, 3]
+    end
+
+    it 'keeps position of lists when list type is done' do
+      service = described_class.new(project, user, position: 2)
+
+      service.execute(done)
+
+      expect(current_list_positions).to eq [0, 1, 2, 3]
+    end
+  end
+
+  def current_list_positions
+    [planning, development, review, staging].map { |list| list.reload.position }
+  end
+end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index c931c3e4829d2e32bdf5bc0a55ae967fc69c2887..b3e0a7b9b58b19fd4eee1f222083b6e5d37f5a93 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,7 +5,7 @@ module Ci
     let(:service) { ImageForBuildService.new }
     let(:project) { FactoryGirl.create(:empty_project) }
     let(:commit_sha) { '01234567890123456789' }
-    let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+    let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
     let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
 
     describe '#execute' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ad8c2485888d0cf6955662737bbb710ca5c544d9..ff113efd916befea2ec0f05da4582085e88a745d 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
 describe Ci::ProcessPipelineService, services: true do
   let(:pipeline) { create(:ci_pipeline, ref: 'master') }
   let(:user) { create(:user) }
-  let(:all_builds) { pipeline.builds }
-  let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
   let(:config) { nil }
 
   before do
@@ -12,7 +10,15 @@ describe Ci::ProcessPipelineService, services: true do
   end
 
   describe '#execute' do
-    def create_builds
+    def all_builds
+      pipeline.builds
+    end
+
+    def builds
+      all_builds.where.not(status: [:created, :skipped])
+    end
+
+    def process_pipeline
       described_class.new(pipeline.project, user).execute(pipeline)
     end
 
@@ -30,26 +36,26 @@ describe Ci::ProcessPipelineService, services: true do
       end
 
       it 'processes a pipeline' do
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         succeed_pending
         expect(builds.success.count).to eq(2)
 
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         succeed_pending
         expect(builds.success.count).to eq(4)
 
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         succeed_pending
         expect(builds.success.count).to eq(5)
 
-        expect(create_builds).to be_falsey
+        expect(process_pipeline).to be_falsey
       end
 
       it 'does not process pipeline if existing stage is running' do
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         expect(builds.pending.count).to eq(2)
-        
-        expect(create_builds).to be_falsey
+
+        expect(process_pipeline).to be_falsey
         expect(builds.pending.count).to eq(2)
       end
     end
@@ -61,7 +67,7 @@ describe Ci::ProcessPipelineService, services: true do
       end
 
       it 'automatically triggers a next stage when build finishes' do
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         expect(builds.pluck(:status)).to contain_exactly('pending')
 
         pipeline.builds.running_or_pending.each(&:drop)
@@ -82,7 +88,7 @@ describe Ci::ProcessPipelineService, services: true do
 
       context 'when builds are successful' do
         it 'properly creates builds' do
-          expect(create_builds).to be_truthy
+          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)
@@ -107,7 +113,7 @@ describe Ci::ProcessPipelineService, services: true do
 
       context 'when test job fails' do
         it 'properly creates builds' do
-          expect(create_builds).to be_truthy
+          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)
@@ -132,7 +138,7 @@ describe Ci::ProcessPipelineService, services: true do
 
       context 'when test and test_failure jobs fail' do
         it 'properly creates builds' do
-          expect(create_builds).to be_truthy
+          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)
@@ -158,7 +164,7 @@ describe Ci::ProcessPipelineService, services: true do
 
       context 'when deploy job fails' do
         it 'properly creates builds' do
-          expect(create_builds).to be_truthy
+          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)
@@ -183,7 +189,7 @@ describe Ci::ProcessPipelineService, services: true do
 
       context 'when build is canceled in the second stage' do
         it 'does not schedule builds after build has been canceled' do
-          expect(create_builds).to be_truthy
+          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)
@@ -202,7 +208,7 @@ describe Ci::ProcessPipelineService, services: true do
       context 'when listing manual actions' do
         it 'returns only for skipped builds' do
           # currently all builds are created
-          expect(create_builds).to be_truthy
+          expect(process_pipeline).to be_truthy
           expect(manual_actions).to be_empty
 
           # succeed stage build
@@ -224,6 +230,103 @@ describe Ci::ProcessPipelineService, services: true do
       end
     end
 
+    context 'when there are manual/on_failure jobs in earlier stages' do
+      before do
+        builds
+        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 'starts from the second stage' do
+          expect(builds.map(&:status)).to eq(%w[skipped pending created])
+        end
+      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
+
+        it 'skips second stage and continues on third stage' do
+          expect(builds.map(&:status)).to eq(%w[pending created created])
+
+          builds.first.success
+          builds.each(&:reload)
+
+          expect(builds.map(&:status)).to eq(%w[success skipped pending])
+        end
+      end
+
+      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
+
+        it 'skips second stage and continues on third stage' do
+          expect(builds.map(&:status)).to eq(%w[pending created created])
+
+          builds.first.success
+          builds.each(&:reload)
+
+          expect(builds.map(&:status)).to eq(%w[success skipped pending])
+        end
+      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)
+      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
+
+        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')
+
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name))
+            .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+          pipeline.builds.find_by(name: 'test:1').success
+          pipeline.builds.find_by(name: 'test:2').drop
+
+          expect(builds.pluck(:name))
+            .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+          Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+          expect(builds.pluck(:name)).to contain_exactly(
+            'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+        end
+      end
+    end
+
     context 'creates a builds from .gitlab-ci.yml' do
       let(:config) do
         YAML.dump({
@@ -257,14 +360,14 @@ describe Ci::ProcessPipelineService, services: true do
         expect(all_builds.count).to eq(2)
 
         # Create builds will mark the created as pending
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         expect(builds.count).to eq(2)
         expect(all_builds.count).to eq(2)
 
         # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
         # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
         succeed_pending
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         expect(builds.success.count).to eq(2)
         expect(builds.pending.count).to eq(2)
         expect(all_builds.count).to eq(5)
@@ -272,14 +375,14 @@ describe Ci::ProcessPipelineService, services: true do
         # When we succeed the 2 pending from stage test,
         # We will queue a deploy stage, no new builds will be created
         succeed_pending
-        expect(create_builds).to be_truthy
+        expect(process_pipeline).to be_truthy
         expect(builds.pending.count).to eq(1)
         expect(builds.success.count).to eq(4)
         expect(all_builds.count).to eq(5)
 
         # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
         succeed_pending
-        expect(create_builds).to be_falsey
+        expect(process_pipeline).to be_falsey
         expect(builds.success.count).to eq(5)
         expect(all_builds.count).to eq(5)
       end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index 026d0ca65340146400d3c9f1a43f244eb44a1bf4..a3fc23ba17783fa63d8fb9f34c401690ebb1d772 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -101,11 +101,11 @@ module Ci
           it 'equalises number of running builds' do
             # after finishing the first build for project 1, get a second build from the same project
             expect(service.execute(shared_runner)).to eq(build1_project1)
-            build1_project1.success
+            build1_project1.reload.success
             expect(service.execute(shared_runner)).to eq(build2_project1)
 
             expect(service.execute(shared_runner)).to eq(build1_project2)
-            build1_project2.success
+            build1_project2.reload.success
             expect(service.execute(shared_runner)).to eq(build2_project2)
             expect(service.execute(shared_runner)).to eq(build1_project3)
             expect(service.execute(shared_runner)).to eq(build3_project1)
@@ -151,6 +151,25 @@ module Ci
           it { expect(build.runner).to eq(specific_runner) }
         end
       end
+
+      context 'disallow when builds are disabled' do
+        before do
+          project.update(shared_runners_enabled: true)
+          project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+        end
+
+        context 'and uses shared runner' do
+          let(:build) { service.execute(shared_runner) }
+
+          it { expect(build).to be_nil }
+        end
+
+        context 'and uses specific runner' do
+          let(:build) { service.execute(specific_runner) }
+
+          it { expect(build).to be_nil }
+        end
+      end
     end
   end
 end
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3760f19aaa2eedfd54da5f6644e5b408dd708250
--- /dev/null
+++ b/spec/services/compare_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe CompareService, services: true do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:service) { described_class.new }
+
+  describe '#execute' do
+    context 'compare with base, like feature...fix' do
+      subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+
+      it { expect(subject.diffs.size).to eq(1) }
+    end
+
+    context 'straight compare, like feature..fix' do
+      subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+
+      it { expect(subject.diffs.size).to eq(3) }
+    end
+  end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 8da2a2b3c1b2c430d51cd93bce5c09383abf8b79..cf0a18aacec705ceeae36eab5920242e770e88bf 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do
   let(:service) { described_class.new(project, user, params) }
 
   describe '#execute' do
+    let(:options) { nil }
     let(:params) do
       { environment: 'production',
         ref: 'master',
         tag: false,
         sha: '97de212e80737a608d939f648d959671fb0a0142',
+        options: options
       }
     end
 
@@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do
     end
 
     context 'when environment exist' do
-      before { create(:environment, project: project, name: 'production') }
+      let!(:environment) { create(:environment, project: project, name: 'production') }
 
       it 'does not create a new environment' do
         expect { subject }.not_to change { Environment.count }
@@ -37,11 +39,51 @@ describe CreateDeploymentService, services: true do
       it 'does create a deployment' do
         expect(subject).to be_persisted
       end
+
+      context 'and start action is defined' do
+        let(:options) { { action: 'start' } }
+
+        context 'and environment is stopped' do
+          before do
+            environment.stop
+          end
+
+          it 'makes environment available' do
+            subject
+
+            expect(environment.reload).to be_available
+          end
+
+          it 'does create a deployment' do
+            expect(subject).to be_persisted
+          end
+        end
+      end
+
+      context 'and stop action is defined' do
+        let(:options) { { action: 'stop' } }
+
+        context 'and environment is available' do
+          before do
+            environment.start
+          end
+
+          it 'makes environment stopped' do
+            subject
+
+            expect(environment.reload).to be_stopped
+          end
+
+          it 'does not create a deployment' do
+            expect(subject).to be_nil
+          end
+        end
+      end
     end
 
     context 'for environment with invalid name' do
       let(:params) do
-        { environment: 'name with spaces',
+        { environment: 'name,with,commas',
           ref: 'master',
           tag: false,
           sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -53,14 +95,72 @@ describe CreateDeploymentService, services: true do
       end
 
       it 'does not create a deployment' do
-        expect(subject).not_to be_persisted
+        expect(subject).to be_nil
+      end
+    end
+
+    context 'when variables are used' do
+      let(:params) do
+        { environment: 'review-apps/$CI_BUILD_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'
+          },
+          variables: [
+            { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+          ]
+        }
+      end
+
+      it 'does create a new environment' do
+        expect { subject }.to change { Environment.count }.by(1)
+
+        expect(subject.environment.name).to eq('review-apps/feature-review-apps')
+        expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
+      end
+
+      it 'does create a new deployment' do
+        expect(subject).to be_persisted
+      end
+
+      context 'and environment exist' do
+        let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') }
+
+        it 'does not create a new environment' do
+          expect { subject }.not_to change { Environment.count }
+        end
+
+        it 'updates external url' do
+          subject
+
+          expect(subject.environment.name).to eq('review-apps/feature-review-apps')
+          expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
+        end
+
+        it 'does create a new deployment' do
+          expect(subject).to be_persisted
+        end
+      end
+    end
+
+    context 'when project was removed' do
+      let(:project) { nil }
+
+      it 'does not create deployment or environment' do
+        expect { subject }.not_to raise_error
+
+        expect(Environment.count).to be_zero
+        expect(Deployment.count).to be_zero
       end
     end
   end
-  
+
   describe 'processing of builds' do
     let(:environment) { nil }
-    
+
     shared_examples 'does not create environment and deployment' do
       it 'does not create a new environment' do
         expect { subject }.not_to change { Environment.count }
@@ -95,19 +195,28 @@ describe CreateDeploymentService, services: true do
 
         expect(Deployment.last.deployable).to eq(deployable)
       end
+
+      it 'create environment has URL set' do
+        subject
+
+        expect(Deployment.last.environment.external_url).not_to be_nil
+      end
     end
 
     context 'without environment specified' do
       let(:build) { create(:ci_build, project: project) }
-      
+
       it_behaves_like 'does not create environment and deployment' do
         subject { build.success }
       end
     end
-    
+
     context 'when environment is specified' do
       let(:pipeline) { create(:ci_pipeline, project: project) }
-      let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') }
+      let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
+      let(:options) do
+        { environment: { name: 'production', url: 'http://gitlab.com' } }
+      end
 
       context 'when build succeeds' do
         it_behaves_like 'does create environment and deployment' do
@@ -132,4 +241,83 @@ describe CreateDeploymentService, services: true do
       end
     end
   end
+
+  describe "merge request metrics" do
+    let(:params) do
+      {
+        environment: 'production',
+        ref: 'master',
+        tag: false,
+        sha: '97de212e80737a608d939f648d959671fb0a0142b',
+      }
+    end
+
+    let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
+
+    context "while updating the 'first_deployed_to_production_at' time" do
+      before { merge_request.mark_as_merged }
+
+      context "for merge requests merged before the current deploy" do
+        it "sets the time if the deploy's environment is 'production'" do
+          time = Time.now
+          Timecop.freeze(time) { service.execute }
+
+          expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
+        end
+
+        it "doesn't set the time if the deploy's environment is not 'production'" do
+          staging_params = params.merge(environment: 'staging')
+          service = described_class.new(project, user, staging_params)
+          service.execute
+
+          expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+        end
+
+        it 'does not raise errors if the merge request does not have a metrics record' do
+          merge_request.metrics.destroy
+
+          expect(merge_request.reload.metrics).to be_nil
+          expect { service.execute }.not_to raise_error
+        end
+      end
+
+      context "for merge requests merged before the previous deploy" do
+        context "if the 'first_deployed_to_production_at' time is already set" do
+          it "does not overwrite the older 'first_deployed_to_production_at' time" do
+            # Previous deploy
+            time = Time.now
+            Timecop.freeze(time) { service.execute }
+
+            expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
+
+            # Current deploy
+            service = described_class.new(project, user, params)
+            Timecop.freeze(time + 12.hours) { service.execute }
+
+            expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
+          end
+        end
+
+        context "if the 'first_deployed_to_production_at' time is not already set" do
+          it "does not overwrite the older 'first_deployed_to_production_at' time" do
+            # Previous deploy
+            time = 5.minutes.from_now
+            Timecop.freeze(time) { service.execute }
+
+            expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
+
+            merge_request.reload.metrics.update(first_deployed_to_production_at: nil)
+
+            expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+
+            # Current deploy
+            service = described_class.new(project, user, params)
+            Timecop.freeze(time + 12.hours) { service.execute }
+
+            expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+          end
+        end
+      end
+    end
+  end
 end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 16a9956fe7f998cb3c9cef56fe461a89f75a5eb7..b7dc99ed88710ed811552dcf7216c316f9506546 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -110,4 +110,23 @@ describe EventCreateService, services: true do
       end
     end
   end
+
+  describe 'Project' do
+    let(:user) { create :user }
+    let(:project) { create(:empty_project) }
+
+    describe '#join_project' do
+      subject { service.join_project(project, user) }
+
+      it { is_expected.to be_truthy }
+      it { expect { subject }.to change { Event.count }.from(0).to(1) }
+    end
+
+    describe '#expired_leave_project' do
+      subject { service.expired_leave_project(project, user) }
+
+      it { is_expected.to be_truthy }
+      it { expect { subject }.to change { Event.count }.from(0).to(1) }
+    end
+  end
 end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d019e50649f20eb18770829199a6e2b46293f0b0..d3c37c7820f6ebdcafe4d48fd34d1427506b8cb5 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -41,7 +41,7 @@ describe Files::UpdateService do
       it "returns a hash with the :success status " do
         results = subject.execute
 
-        expect(results).to match({ status: :success })
+        expect(results[:status]).to match(:success)
       end
 
       it "updates the file with the new contents" do
@@ -69,7 +69,7 @@ describe Files::UpdateService do
       it "returns a hash with the :success status " do
         results = subject.execute
 
-        expect(results).to match({ status: :success })
+        expect(results[:status]).to match(:success)
       end
 
       it "updates the file with the new contents" do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 6ac1fa8f18295373330e271f14183acc7ac90c6f..cea7e6429f953da7f9601edd1fb8f0ad69c78c3a 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -184,8 +184,8 @@ describe GitPushService, services: true do
 
     context "Updates merge requests" do
       it "when pushing a new branch for the first time" do
-        expect(project).to receive(:update_merge_requests).
-                               with(@blankrev, 'newrev', 'refs/heads/master', user)
+        expect(UpdateMergeRequestsWorker).to receive(:perform_async).
+                                                with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master')
         execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
       end
     end
@@ -253,6 +253,21 @@ describe GitPushService, services: true do
         expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
       end
 
+      it "when pushing a branch for the first time with an existing branch permission configured" do
+        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+        create(:protected_branch, :no_one_can_push, :developers_can_merge, project: project, name: 'master')
+        expect(project).to receive(:execute_hooks)
+        expect(project.default_branch).to eq("master")
+        expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute)
+
+        execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+
+        expect(project.protected_branches).not_to be_empty
+        expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
+        expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+      end
+
       it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
         stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
 
@@ -287,6 +302,9 @@ describe GitPushService, services: true do
         author_email: commit_author.email
       )
 
+      allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+        and_return(commit)
+
       allow(project.repository).to receive(:commits_between).and_return([commit])
     end
 
@@ -324,6 +342,46 @@ describe GitPushService, services: true do
     end
   end
 
+  describe "issue metrics" do
+    let(:issue) { create :issue, project: project }
+    let(:commit_author) { create :user }
+    let(:commit) { project.commit }
+    let(:commit_time) { Time.now }
+
+    before do
+      project.team << [commit_author, :developer]
+      project.team << [user, :developer]
+
+      allow(commit).to receive_messages(
+        safe_message: "this commit \n mentions #{issue.to_reference}",
+        references: [issue],
+        author_name: commit_author.name,
+        author_email: commit_author.email,
+        committed_date: commit_time
+      )
+
+      allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+        and_return(commit)
+
+      allow(project.repository).to receive(:commits_between).and_return([commit])
+    end
+
+    context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
+      it 'sets the metric for referenced issues' do
+        execute_service(project, user, @oldrev, @newrev, @ref)
+
+        expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
+      end
+
+      it 'does not set the metric for non-referenced issues' do
+        non_referenced_issue = create(:issue, project: project)
+        execute_service(project, user, @oldrev, @newrev, @ref)
+
+        expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
+      end
+    end
+  end
+
   describe "closing issues from pushed commits containing a closing reference" do
     let(:issue) { create :issue, project: project }
     let(:other_issue) { create :issue, project: project }
@@ -341,6 +399,9 @@ describe GitPushService, services: true do
       allow(project.repository).to receive(:commits_between).
         and_return([closing_commit])
 
+      allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+        and_return(closing_commit)
+
       project.team << [commit_author, :master]
     end
 
@@ -363,7 +424,7 @@ describe GitPushService, services: true do
       it "doesn't close issues when external issue tracker is in use" do
         allow_any_instance_of(Project).to receive(:default_issues_tracker?).
           and_return(false)
-        external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid)
+        external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
         allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
 
         # The push still shouldn't create cross-reference notes.
@@ -396,12 +457,10 @@ describe GitPushService, services: true do
       let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
 
       before do
+        # project.create_jira_service doesn't seem to invalidate the cache here
+        project.has_external_issue_tracker = true
         jira_service_settings
-
-        WebMock.stub_request(:post, jira_api_transition_url)
-        WebMock.stub_request(:post, jira_api_comment_url)
-        WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
-        WebMock.stub_request(:get, jira_api_test_url)
+        stub_jira_urls("JIRA-1")
 
         allow(closing_commit).to receive_messages({
                                                     issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
@@ -421,39 +480,50 @@ describe GitPushService, services: true do
         let(:message) { "this is some work.\n\nrelated to JIRA-1" }
 
         it "initiates one api call to jira server to mention the issue" do
-          execute_service(project, user, @oldrev, @newrev, @ref )
+          execute_service(project, user, @oldrev, @newrev, @ref)
 
-          expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+          expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
             body: /mentioned this issue in/
           ).once
         end
       end
 
       context "closing an issue" do
-        let(:message) { "this is some work.\n\ncloses JIRA-1" }
-
-        it "initiates one api call to jira server to close the issue" do
-          transition_body = {
-            transition: {
-              id: '2'
-            }
-          }.to_json
-
-          execute_service(project, commit_author, @oldrev, @newrev, @ref )
-          expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
-            body: transition_body
-          ).once
+        let(:message)         { "this is some work.\n\ncloses JIRA-1" }
+        let(:comment_body)    { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+
+        context "using right markdown" do
+          it "initiates one api call to jira server to close the issue" do
+            execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+            expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
+          end
+
+          it "initiates one api call to jira server to comment on the issue" do
+            execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+            expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
+              body: comment_body
+            ).once
+          end
         end
 
-        it "initiates one api call to jira server to comment on the issue" do
-          comment_body = {
-            body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
-          }.to_json
+        context "using wrong markdown" do
+          let(:message) { "this is some work.\n\ncloses #1" }
 
-          execute_service(project, commit_author, @oldrev, @newrev, @ref )
-          expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
-            body: comment_body
-          ).once
+          it "does not initiates one api call to jira server to close the issue" do
+            execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+            expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
+          end
+
+          it "does not initiates one api call to jira server to comment on the issue" do
+            execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+            expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
+              body: comment_body
+            ).once
+          end
         end
       end
     end
@@ -477,9 +547,16 @@ describe GitPushService, services: true do
     let(:housekeeping) { Projects::HousekeepingService.new(project) }
 
     before do
+      # Flush any raw Redis data stored by the housekeeping code.
+      Gitlab::Redis.with { |conn| conn.flushall }
+
       allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
     end
 
+    after do
+      Gitlab::Redis.with { |conn| conn.flushall }
+    end
+
     it 'does not perform housekeeping when not needed' do
       expect(housekeeping).not_to receive(:execute)
 
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index a4fcd44882d987294ded49c00902c073c0925355..0879e3ab4c881e8b77efd601eb8ddbd1c6386221 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -37,65 +37,138 @@ describe GitTagPushService, services: true do
   end
 
   describe "Git Tag Push Data" do
-    before do
-      service.execute
-      @push_data = service.push_data
-      @tag_name = Gitlab::Git.ref_name(ref)
-      @tag = project.repository.find_tag(@tag_name)
-      @commit = project.commit(@tag.target)
-    end
-
     subject { @push_data }
+    let(:tag) { project.repository.find_tag(tag_name) }
+    let(:commit) { tag.dereferenced_target }
 
-    it { is_expected.to include(object_kind: 'tag_push') }
-    it { is_expected.to include(ref: ref) }
-    it { is_expected.to include(before: oldrev) }
-    it { is_expected.to include(after: newrev) }
-    it { is_expected.to include(message: @tag.message) }
-    it { is_expected.to include(user_id: user.id) }
-    it { is_expected.to include(user_name: user.name) }
-    it { is_expected.to include(project_id: project.id) }
-
-    context "with repository data" do
-      subject { @push_data[:repository] }
-
-      it { is_expected.to include(name: project.name) }
-      it { is_expected.to include(url: project.url_to_repo) }
-      it { is_expected.to include(description: project.description) }
-      it { is_expected.to include(homepage: project.web_url) }
-    end
+    context 'annotated tag' do
+      let(:tag_name) { Gitlab::Git.ref_name(ref) }
 
-    context "with commits" do
-      subject { @push_data[:commits] }
+      before do
+        service.execute
+        @push_data = service.push_data
+      end
 
-      it { is_expected.to be_an(Array) }
-      it 'has 1 element' do
-        expect(subject.size).to eq(1)
+      it { is_expected.to include(object_kind: 'tag_push') }
+      it { is_expected.to include(ref: ref) }
+      it { is_expected.to include(before: oldrev) }
+      it { is_expected.to include(after: newrev) }
+      it { is_expected.to include(message: tag.message) }
+      it { is_expected.to include(user_id: user.id) }
+      it { is_expected.to include(user_name: user.name) }
+      it { is_expected.to include(project_id: project.id) }
+
+      context "with repository data" do
+        subject { @push_data[:repository] }
+
+        it { is_expected.to include(name: project.name) }
+        it { is_expected.to include(url: project.url_to_repo) }
+        it { is_expected.to include(description: project.description) }
+        it { is_expected.to include(homepage: project.web_url) }
       end
 
-      context "the commit" do
-        subject { @push_data[:commits].first }
-
-        it { is_expected.to include(id: @commit.id) }
-        it { is_expected.to include(message: @commit.safe_message) }
-        it { is_expected.to include(timestamp: @commit.date.xmlschema) }
-        it do
-          is_expected.to include(
-            url: [
-             Gitlab.config.gitlab.url,
-             project.namespace.to_param,
-             project.to_param,
-             'commit',
-             @commit.id
-            ].join('/')
-          )
+      context "with commits" do
+        subject { @push_data[:commits] }
+
+        it { is_expected.to be_an(Array) }
+        it 'has 1 element' do
+          expect(subject.size).to eq(1)
+        end
+
+        context "the commit" do
+          subject { @push_data[:commits].first }
+
+          it { is_expected.to include(id: commit.id) }
+          it { is_expected.to include(message: commit.safe_message) }
+          it { is_expected.to include(timestamp: commit.date.xmlschema) }
+          it do
+            is_expected.to include(
+              url: [
+               Gitlab.config.gitlab.url,
+               project.namespace.to_param,
+               project.to_param,
+               'commit',
+               commit.id
+              ].join('/')
+            )
+          end
+
+          context "with a author" do
+            subject { @push_data[:commits].first[:author] }
+
+            it { is_expected.to include(name: commit.author_name) }
+            it { is_expected.to include(email: commit.author_email) }
+          end
         end
+      end
+    end
 
-        context "with a author" do
-          subject { @push_data[:commits].first[:author] }
+    context 'lightweight tag' do
+      let(:tag_name) { 'light-tag' }
+      let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+      let(:ref) { "refs/tags/light-tag" }
+
+      before do
+        # Create the lightweight tag
+        project.repository.raw_repository.rugged.tags.create(tag_name, newrev)
+
+        # Clear tag list cache
+        project.repository.expire_tags_cache
+
+        service.execute
+        @push_data = service.push_data
+      end
+
+      it { is_expected.to include(object_kind: 'tag_push') }
+      it { is_expected.to include(ref: ref) }
+      it { is_expected.to include(before: oldrev) }
+      it { is_expected.to include(after: newrev) }
+      it { is_expected.to include(message: tag.message) }
+      it { is_expected.to include(user_id: user.id) }
+      it { is_expected.to include(user_name: user.name) }
+      it { is_expected.to include(project_id: project.id) }
+
+      context "with repository data" do
+        subject { @push_data[:repository] }
+
+        it { is_expected.to include(name: project.name) }
+        it { is_expected.to include(url: project.url_to_repo) }
+        it { is_expected.to include(description: project.description) }
+        it { is_expected.to include(homepage: project.web_url) }
+      end
+
+      context "with commits" do
+        subject { @push_data[:commits] }
+
+        it { is_expected.to be_an(Array) }
+        it 'has 1 element' do
+          expect(subject.size).to eq(1)
+        end
 
-          it { is_expected.to include(name: @commit.author_name) }
-          it { is_expected.to include(email: @commit.author_email) }
+        context "the commit" do
+          subject { @push_data[:commits].first }
+
+          it { is_expected.to include(id: commit.id) }
+          it { is_expected.to include(message: commit.safe_message) }
+          it { is_expected.to include(timestamp: commit.date.xmlschema) }
+          it do
+            is_expected.to include(
+              url: [
+               Gitlab.config.gitlab.url,
+               project.namespace.to_param,
+               project.to_param,
+               'commit',
+               commit.id
+              ].join('/')
+            )
+          end
+
+          context "with a author" do
+            subject { @push_data[:commits].first[:author] }
+
+            it { is_expected.to include(name: commit.author_name) }
+            it { is_expected.to include(email: commit.author_email) }
+          end
         end
       end
     end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
similarity index 97%
rename from spec/services/issues/bulk_update_service_spec.rb
rename to spec/services/issuable/bulk_update_service_spec.rb
index ac08aa53b0ba04129b07fc57d6d4fc0729a5317a..6f7ce8ca992018a3826359dd6ea5d04b7082e4e5 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -1,14 +1,14 @@
 require 'spec_helper'
 
-describe Issues::BulkUpdateService, services: true do
+describe Issuable::BulkUpdateService, services: true do
   let(:user)    { create(:user) }
   let(:project) { create(:empty_project, namespace: user.namespace) }
 
   def bulk_update(issues, extra_params = {})
     bulk_update_params = extra_params
-      .reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
+      .reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
 
-    Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
+    Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
   end
 
   describe 'close issues' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 1318607a388015b0612c2f5dc0c9bb6cb209a62b..4465f22a001ac48873168bff0daf5ce252475970 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
 describe Issues::CloseService, services: true do
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
+  let(:guest) { create(:user) }
   let(:issue) { create(:issue, assignee: user2) }
   let(:project) { issue.project }
   let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -10,18 +11,48 @@ describe Issues::CloseService, services: true do
   before do
     project.team << [user, :master]
     project.team << [user2, :developer]
+    project.team << [guest, :guest]
   end
 
   describe '#execute' do
+    let(:service) { described_class.new(project, user) }
+
+    it 'checks if the user is authorized to update the issue' do
+      expect(service).to receive(:can?).with(user, :update_issue, issue).
+        and_call_original
+
+      service.execute(issue)
+    end
+
+    it 'does not close the issue when the user is not authorized to do so' do
+      allow(service).to receive(:can?).with(user, :update_issue, issue).
+        and_return(false)
+
+      expect(service).not_to receive(:close_issue)
+      expect(service.execute(issue)).to eq(issue)
+    end
+
+    it 'closes the issue when the user is authorized to do so' do
+      allow(service).to receive(:can?).with(user, :update_issue, issue).
+        and_return(true)
+
+      expect(service).to receive(:close_issue).
+        with(issue, commit: nil, notifications: true, system_note: true)
+
+      service.execute(issue)
+    end
+  end
+
+  describe '#close_issue' do
     context "valid params" do
       before do
         perform_enqueued_jobs do
-          @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+          described_class.new(project, user).close_issue(issue)
         end
       end
 
-      it { expect(@issue).to be_valid }
-      it { expect(@issue).to be_closed }
+      it { expect(issue).to be_valid }
+      it { expect(issue).to be_closed }
 
       it 'sends email to user2 about assign of new issue' do
         email = ActionMailer::Base.deliveries.last
@@ -30,7 +61,7 @@ describe Issues::CloseService, services: true do
       end
 
       it 'creates system note about issue reassign' do
-        note = @issue.notes.last
+        note = issue.notes.last
         expect(note.note).to include "Status changed to closed"
       end
 
@@ -39,14 +70,34 @@ describe Issues::CloseService, services: true do
       end
     end
 
-    context "external issue tracker" do
+    context 'when issue is not confidential' do
+      it 'executes issue hooks' do
+        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+        expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+        described_class.new(project, user).close_issue(issue)
+      end
+    end
+
+    context 'when issue is confidential' do
+      it 'executes confidential issue hooks' do
+        issue = create(:issue, :confidential, project: project)
+
+        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+        expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+        described_class.new(project, user).close_issue(issue)
+      end
+    end
+
+    context 'external issue tracker' do
       before do
         allow(project).to receive(:default_issues_tracker?).and_return(false)
-        @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+        described_class.new(project, user).close_issue(issue)
       end
 
-      it { expect(@issue).to be_valid }
-      it { expect(@issue).to be_opened }
+      it { expect(issue).to be_valid }
+      it { expect(issue).to be_opened }
       it { expect(todo.reload).to be_pending }
     end
   end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1ee9f3aae4dcb17aaf754121e33e75c4381df444..5c0331ebe66c154944a8914e0f605563321ee672 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -20,16 +20,38 @@ describe Issues::CreateService, services: true do
       let(:opts) do
         { title: 'Awesome issue',
           description: 'please fix',
-          assignee: assignee,
+          assignee_id: assignee.id,
           label_ids: labels.map(&:id),
-          milestone_id: milestone.id }
+          milestone_id: milestone.id,
+          due_date: Date.tomorrow }
       end
 
-      it { expect(issue).to be_valid }
-      it { expect(issue.title).to eq('Awesome issue') }
-      it { expect(issue.assignee).to eq assignee }
-      it { expect(issue.labels).to match_array labels }
-      it { expect(issue.milestone).to eq milestone }
+      it 'creates the issue with the given params' do
+        expect(issue).to be_persisted
+        expect(issue.title).to eq('Awesome issue')
+        expect(issue.assignee).to eq assignee
+        expect(issue.labels).to match_array labels
+        expect(issue.milestone).to eq milestone
+        expect(issue.due_date).to eq Date.tomorrow
+      end
+
+      context 'when current user cannot admin issues in the project' do
+        let(:guest) { create(:user) }
+        before do
+          project.team << [guest, :guest]
+        end
+
+        it 'filters out params that cannot be set without the :admin_issue permission' do
+          issue = described_class.new(project, guest, opts).execute
+
+          expect(issue).to be_persisted
+          expect(issue.title).to eq('Awesome issue')
+          expect(issue.assignee).to be_nil
+          expect(issue.labels).to be_empty
+          expect(issue.milestone).to be_nil
+          expect(issue.due_date).to be_nil
+        end
+      end
 
       it 'creates a pending todo for new assignee' do
         attributes = {
@@ -45,6 +67,27 @@ describe Issues::CreateService, services: true do
         expect(Todo.where(attributes).count).to eq 1
       end
 
+      context 'when label belongs to project group' do
+        let(:group) { create(:group) }
+        let(:group_labels) { create_pair(:group_label, group: group) }
+
+        let(:opts) do
+          {
+            title: 'Title',
+            description: 'Description',
+            label_ids: group_labels.map(&:id)
+          }
+        end
+
+        before do
+          project.update(group: group)
+        end
+
+        it 'assigns group labels' do
+          expect(issue.labels).to match_array group_labels
+        end
+      end
+
       context 'when label belongs to different project' do
         let(:label) { create(:label) }
 
@@ -72,6 +115,26 @@ describe Issues::CreateService, services: true do
           expect(issue.milestone).not_to eq milestone
         end
       end
+
+      it 'executes issue hooks when issue is not confidential' do
+        opts = { title: 'Title', description: 'Description', confidential: false }
+
+        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+        expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+        described_class.new(project, user, opts).execute
+      end
+
+      it 'executes confidential issue hooks when issue is confidential' do
+        opts = { title: 'Title', description: 'Description', confidential: true }
+
+        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+        expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+        described_class.new(project, user, opts).execute
+      end
     end
+
+    it_behaves_like 'new issuable record that supports slash commands'
   end
 end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 93bf0f649634e4d0cb4e338841e3626abecd4d12..f0ded06b78504428863a4c44b9794040b1ae8d2e 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do
       old_project.team << [user, :reporter]
       new_project.team << [user, :reporter]
 
-      ['label1', 'label2'].each do |label|
+      labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+      labels.each do |label|
         old_issue.labels << create(:label,
           project_id: old_project.id,
           title: label)
-      end
 
-      new_project.labels << create(:label, title: 'label1')
-      new_project.labels << create(:label, title: 'label2')
+        new_project.labels << create(:label, title: label)
+      end
     end
   end
 
@@ -207,10 +208,10 @@ describe Issues::MoveService, services: true do
         end
       end
 
-      describe 'rewritting references' do
+      describe 'rewriting references' do
         include_context 'issue move executed'
 
-        context 'issue reference' do
+        context 'issue references' do
           let(:another_issue) { create(:issue, project: old_project) }
           let(:description) { "Some description #{another_issue.to_reference}" }
 
@@ -219,6 +220,16 @@ describe Issues::MoveService, services: true do
               .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}"
           end
         end
+
+        context "user references" do
+          let(:another_issue) { create(:issue, project: old_project) }
+          let(:description) { "Some description #{user.to_reference}" }
+
+          it "doesn't throw any errors for issues containing user references" do
+            expect(new_issue.description)
+              .to eq "Some description #{user.to_reference}"
+          end
+        end
       end
 
       context 'moving to same project' do
@@ -277,5 +288,25 @@ describe Issues::MoveService, services: true do
         it { expect { move }.to raise_error(StandardError, /permissions/) }
       end
     end
+
+    context 'movable issue with no assigned labels' do
+      before do
+        old_project.team << [user, :reporter]
+        new_project.team << [user, :reporter]
+
+        labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+        labels.each do |label|
+          new_project.labels << create(:label, title: label)
+        end
+      end
+
+      include_context 'issue move executed'
+
+      it 'does not assign labels to new issue' do
+        expected_label_titles = new_issue.reload.labels.map(&:title)
+        expect(expected_label_titles.size).to eq 0
+      end
+    end
   end
 end
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93a8270fd16e477c97aab4842539e19eb76c032b
--- /dev/null
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Issues::ReopenService, services: true do
+  let(:project) { create(:empty_project) }
+  let(:issue) { create(:issue, :closed, project: project) }
+
+  describe '#execute' do
+    context 'when user is not authorized to reopen issue' do
+      before do
+        guest = create(:user)
+        project.team << [guest, :guest]
+
+        perform_enqueued_jobs do
+          described_class.new(project, guest).execute(issue)
+        end
+      end
+
+      it 'does not reopen the issue' do
+        expect(issue).to be_closed
+      end
+    end
+
+    context 'when user is authrized to reopen issue' do
+      let(:user) { create(:user) }
+
+      before do
+        project.team << [user, :master]
+      end
+
+      context 'when issue is not confidential' do
+        it 'executes issue hooks' do
+          expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+          expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+          described_class.new(project, user).execute(issue)
+        end
+      end
+
+      context 'when issue is confidential' do
+        it 'executes confidential issue hooks' do
+          issue = create(:issue, :confidential, :closed, project: project)
+
+          expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+          expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+          described_class.new(project, user).execute(issue)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 088c3d48bf76d2cc74c0fab7bd04c97bcd16b5fa..1638a46ed51d34d23ae2cbd16c7cfeb8250bedbb 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -23,76 +23,123 @@ describe Issues::UpdateService, services: true do
 
   describe 'execute' do
     def find_note(starting_with)
-      @issue.notes.find do |note|
+      issue.notes.find do |note|
         note && note.note.start_with?(starting_with)
       end
     end
 
-    context "valid params" do
-      before do
-        opts = {
+    def update_issue(opts)
+      described_class.new(project, user, opts).execute(issue)
+    end
+
+    context 'valid params' do
+      let(:opts) do
+        {
           title: 'New title',
           description: 'Also please fix',
           assignee_id: user2.id,
           state_event: 'close',
           label_ids: [label.id],
-          confidential: true
+          due_date: Date.tomorrow
         }
+      end
 
-        perform_enqueued_jobs do
-          @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
-        end
+      it 'updates the issue with the given params' do
+        update_issue(opts)
 
-        @issue.reload
+        expect(issue).to be_valid
+        expect(issue.title).to eq 'New title'
+        expect(issue.description).to eq 'Also please fix'
+        expect(issue.assignee).to eq user2
+        expect(issue).to be_closed
+        expect(issue.labels).to match_array [label]
+        expect(issue.due_date).to eq Date.tomorrow
       end
 
-      it { expect(@issue).to be_valid }
-      it { expect(@issue.title).to eq('New title') }
-      it { expect(@issue.assignee).to eq(user2) }
-      it { expect(@issue).to be_closed }
-      it { expect(@issue.labels.count).to eq(1) }
-      it { expect(@issue.labels.first.title).to eq(label.name) }
-
-      it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
-        deliveries = ActionMailer::Base.deliveries
-        email = deliveries.last
-        recipients = deliveries.last(2).map(&:to).flatten
-        expect(recipients).to include(user2.email, user3.email)
-        expect(email.subject).to include(issue.title)
-      end
+      context 'when current user cannot admin issues in the project' do
+        let(:guest) { create(:user) }
+        before do
+          project.team << [guest, :guest]
+        end
 
-      it 'creates system note about issue reassign' do
-        note = find_note('Reassigned to')
+        it 'filters out params that cannot be set without the :admin_issue permission' do
+          described_class.new(project, guest, opts).execute(issue)
 
-        expect(note).not_to be_nil
-        expect(note.note).to include "Reassigned to \@#{user2.username}"
+          expect(issue).to be_valid
+          expect(issue.title).to eq 'New title'
+          expect(issue.description).to eq 'Also please fix'
+          expect(issue.assignee).to eq user3
+          expect(issue.labels).to be_empty
+          expect(issue.milestone).to be_nil
+          expect(issue.due_date).to be_nil
+        end
       end
 
-      it 'creates system note about issue label edit' do
-        note = find_note('Added ~')
+      context 'with background jobs processed' do
+        before do
+          perform_enqueued_jobs do
+            update_issue(opts)
+          end
+        end
 
-        expect(note).not_to be_nil
-        expect(note.note).to include "Added ~#{label.id} label"
-      end
+        it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
+          deliveries = ActionMailer::Base.deliveries
+          email = deliveries.last
+          recipients = deliveries.last(2).map(&:to).flatten
+          expect(recipients).to include(user2.email, user3.email)
+          expect(email.subject).to include(issue.title)
+        end
 
-      it 'creates system note about title change' do
-        note = find_note('Changed title:')
+        it 'creates system note about issue reassign' do
+          note = find_note('Reassigned to')
 
-        expect(note).not_to be_nil
-        expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+          expect(note).not_to be_nil
+          expect(note.note).to include "Reassigned to \@#{user2.username}"
+        end
+
+        it 'creates system note about issue label edit' do
+          note = find_note('Added ~')
+
+          expect(note).not_to be_nil
+          expect(note.note).to include "Added ~#{label.id} label"
+        end
+
+        it 'creates system note about title change' do
+          note = find_note('Changed title:')
+
+          expect(note).not_to be_nil
+          expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+        end
+      end
+    end
+
+    context 'when issue turns confidential' do
+      let(:opts) do
+        {
+          title: 'New title',
+          description: 'Also please fix',
+          assignee_id: user2.id,
+          state_event: 'close',
+          label_ids: [label.id],
+          confidential: true
+        }
       end
 
       it 'creates system note about confidentiality change' do
+        update_issue(confidential: true)
+
         note = find_note('Made the issue confidential')
 
         expect(note).not_to be_nil
         expect(note.note).to eq 'Made the issue confidential'
       end
-    end
 
-    def update_issue(opts)
-      @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
-      @issue.reload
+      it 'executes confidential issue hooks' do
+        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+        expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+        update_issue(confidential: true)
+      end
     end
 
     context 'todos' do
@@ -100,7 +147,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when the title change' do
         before do
-          update_issue({ title: 'New title' })
+          update_issue(title: 'New title')
         end
 
         it 'marks pending todos as done' do
@@ -110,7 +157,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when the description change' do
         before do
-          update_issue({ description: 'Also please fix' })
+          update_issue(description: 'Also please fix')
         end
 
         it 'marks todos as done' do
@@ -120,7 +167,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when is reassigned' do
         before do
-          update_issue({ assignee: user2 })
+          update_issue(assignee: user2)
         end
 
         it 'marks previous assignee todos as done' do
@@ -144,7 +191,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when the milestone change' do
         before do
-          update_issue({ milestone: create(:milestone) })
+          update_issue(milestone: create(:milestone))
         end
 
         it 'marks todos as done' do
@@ -154,7 +201,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when the labels change' do
         before do
-          update_issue({ label_ids: [label.id] })
+          update_issue(label_ids: [label.id])
         end
 
         it 'marks todos as done' do
@@ -165,6 +212,7 @@ describe Issues::UpdateService, services: true do
 
     context 'when the issue is relabeled' do
       let!(:non_subscriber) { create(:user) }
+
       let!(:subscriber) do
         create(:user).tap do |u|
           label.toggle_subscription(u)
@@ -176,7 +224,7 @@ describe Issues::UpdateService, services: true do
         opts = { label_ids: [label.id] }
 
         perform_enqueued_jobs do
-          @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+          @issue = described_class.new(project, user, opts).execute(issue)
         end
 
         should_email(subscriber)
@@ -190,7 +238,7 @@ describe Issues::UpdateService, services: true do
           opts = { label_ids: [label.id, label2.id] }
 
           perform_enqueued_jobs do
-            @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+            @issue = described_class.new(project, user, opts).execute(issue)
           end
 
           should_not_email(subscriber)
@@ -201,7 +249,7 @@ describe Issues::UpdateService, services: true do
           opts = { label_ids: [label2.id] }
 
           perform_enqueued_jobs do
-            @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+            @issue = described_class.new(project, user, opts).execute(issue)
           end
 
           should_not_email(subscriber)
@@ -210,13 +258,15 @@ describe Issues::UpdateService, services: true do
       end
     end
 
-    context 'when Issue has tasks' do
-      before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+    context 'when issue has tasks' do
+      before do
+        update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
+      end
 
-      it { expect(@issue.tasks?).to eq(true) }
+      it { expect(issue.tasks?).to eq(true) }
 
       context 'when tasks are marked as completed' do
-        before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) }
+        before { update_issue(description: "- [x] Task 1\n- [X] Task 2") }
 
         it 'creates system note about task status change' do
           note1 = find_note('Marked the task **Task 1** as completed')
@@ -229,8 +279,8 @@ describe Issues::UpdateService, services: true do
 
       context 'when tasks are marked as incomplete' do
         before do
-          update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
-          update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" })
+          update_issue(description: "- [x] Task 1\n- [X] Task 2")
+          update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
         end
 
         it 'creates system note about task status change' do
@@ -244,8 +294,8 @@ describe Issues::UpdateService, services: true do
 
       context 'when tasks position has been modified' do
         before do
-          update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
-          update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" })
+          update_issue(description: "- [x] Task 1\n- [X] Task 2")
+          update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2")
         end
 
         it 'does not create a system note' do
@@ -257,8 +307,8 @@ describe Issues::UpdateService, services: true do
 
       context 'when a Task list with a completed item is totally replaced' do
         before do
-          update_issue({ description: "- [ ] Task 1\n- [X] Task 2" })
-          update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+          update_issue(description: "- [ ] Task 1\n- [X] Task 2")
+          update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three")
         end
 
         it 'does not create a system note referencing the position the old item' do
@@ -269,7 +319,7 @@ describe Issues::UpdateService, services: true do
 
         it 'does not generate a new note at all' do
           expect do
-            update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+            update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three")
           end.not_to change { Note.count }
         end
       end
@@ -277,7 +327,7 @@ describe Issues::UpdateService, services: true do
 
     context 'updating labels' do
       let(:label3) { create(:label, project: project) }
-      let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
+      let(:result) { described_class.new(project, user, params).execute(issue).reload }
 
       context 'when add_label_ids and label_ids are passed' do
         let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
@@ -319,5 +369,10 @@ describe Issues::UpdateService, services: true do
         end
       end
     end
+
+    context 'updating mentions' do
+      let(:mentionable) { issue }
+      include_examples 'updating mentions', Issues::UpdateService
+    end
   end
 end
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7a9b34f9f963bb9c7329a5fbc8d67e6b7c51422a
--- /dev/null
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Labels::FindOrCreateService, services: true do
+  describe '#execute' do
+    let(:group)   { create(:group) }
+    let(:project) { create(:project, namespace: group) }
+
+    let(:params) do
+      {
+        title: 'Security',
+        description: 'Security related stuff.',
+        color: '#FF0000'
+      }
+    end
+
+    context 'when acting on behalf of a specific user' do
+      let(:user) { create(:user) }
+      subject(:service) { described_class.new(user, project, params) }
+      before do
+        project.team << [user, :developer]
+      end
+
+      context 'when label does not exist at group level' do
+        it 'creates a new label at project level' do
+          expect { service.execute }.to change(project.labels, :count).by(1)
+        end
+      end
+
+      context 'when label exists at group level' do
+        it 'returns the group label' do
+          group_label = create(:group_label, group: group, title: 'Security')
+
+          expect(service.execute).to eq group_label
+        end
+      end
+
+      context 'when label does not exist at group level' do
+        it 'creates a new label at project leve' do
+          expect { service.execute }.to change(project.labels, :count).by(1)
+        end
+      end
+
+      context 'when label exists at project level' do
+        it 'returns the project label' do
+          project_label = create(:label, project: project, title: 'Security')
+
+          expect(service.execute).to eq project_label
+        end
+      end
+    end
+
+    context 'when authorization is not required' do
+      subject(:service) { described_class.new(nil, project, params) }
+
+      it 'returns the project label' do
+        project_label = create(:label, project: project, title: 'Security')
+
+        expect(service.execute(skip_authorization: true)).to eq project_label
+      end
+    end
+  end
+end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddf3527dc0ffee0207549a9d5e28efe4d13807ae
--- /dev/null
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Labels::TransferService, services: true do
+  describe '#execute' do
+    let(:user)    { create(:user) }
+    let(:group_1) { create(:group) }
+    let(:group_2) { create(:group) }
+    let(:group_3) { create(:group) }
+    let(:project_1) { create(:project, namespace: group_2) }
+    let(:project_2) { create(:project, namespace: group_3) }
+
+    let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
+    let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
+    let(:group_label_3) { create(:group_label, group: group_1, name: 'Group Label 3') }
+    let(:group_label_4) { create(:group_label, group: group_2, name: 'Group Label 4') }
+    let(:group_label_5) { create(:group_label, group: group_3, name: 'Group Label 5') }
+    let(:project_label_1) { create(:label, project: project_1, name: 'Project Label 1') }
+
+    subject(:service) { described_class.new(user, group_1, project_1) }
+
+    before do
+      create(:labeled_issue, project: project_1, labels: [group_label_1])
+      create(:labeled_issue, project: project_1, labels: [group_label_4])
+      create(:labeled_issue, project: project_1, labels: [project_label_1])
+      create(:labeled_issue, project: project_2, labels: [group_label_5])
+      create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
+      create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
+    end
+
+    it 'recreates the missing group labels at project level' do
+      expect { service.execute }.to change(project_1.labels, :count).by(2)
+    end
+
+    it 'recreates label priorities related to the missing group labels' do
+      create(:label_priority, project: project_1, label: group_label_1, priority: 1)
+
+      service.execute
+
+      new_project_label = project_1.labels.find_by(title: group_label_1.title)
+      expect(new_project_label.id).not_to eq group_label_1.id
+      expect(new_project_label.priorities).not_to be_empty
+    end
+
+    it 'does not recreate missing group labels that are not applied to issues or merge requests' do
+      service.execute
+
+      expect(project_1.labels.where(title: group_label_3.title)).to be_empty
+    end
+
+    it 'does not recreate missing group labels that already exist in the project group' do
+      service.execute
+
+      expect(project_1.labels.where(title: group_label_4.title)).to be_empty
+    end
+  end
+end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b090343a3e4a0b222f4470d99c93d3b9a772cfb
--- /dev/null
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -0,0 +1,147 @@
+require 'spec_helper'
+
+describe Members::ApproveAccessRequestService, services: true do
+  let(:user) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:group) { create(:group, :public) }
+  let(:opts) { {} }
+
+  shared_examples 'a service raising ActiveRecord::RecordNotFound' do
+    it 'raises ActiveRecord::RecordNotFound' do
+      expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+
+  shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+
+  shared_examples 'a service approving an access request' do
+    it 'succeeds' do
+      expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
+    end
+
+    it 'returns a <Source>Member' do
+      member = described_class.new(source, user, params).execute(opts)
+
+      expect(member).to be_a "#{source.class}Member".constantize
+      expect(member.requested_at).to be_nil
+    end
+
+    context 'with a custom access level' do
+      let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) }
+
+      it 'returns a ProjectMember with the custom access level' do
+        member = described_class.new(source, user, params2).execute(opts)
+
+        expect(member.access_level).to eq Gitlab::Access::MASTER
+      end
+    end
+  end
+
+  context 'when no access requester are found' do
+    let(:params) { { user_id: 42 } }
+
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { project }
+    end
+
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { group }
+    end
+  end
+
+  context 'when an access requester is found' do
+    before do
+      project.request_access(access_requester)
+      group.request_access(access_requester)
+    end
+    let(:params) { { user_id: access_requester.id } }
+
+    context 'when current user is nil' do
+      let(:user) { nil }
+
+      context 'and :force option is not given' do
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { project }
+        end
+
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { group }
+        end
+      end
+
+      context 'and :force option is false' do
+        let(:opts) { { force: false } }
+
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { project }
+        end
+
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { group }
+        end
+      end
+
+      context 'and :force option is true' do
+        let(:opts) { { force: true } }
+
+        it_behaves_like 'a service approving an access request' do
+          let(:source) { project }
+        end
+
+        it_behaves_like 'a service approving an access request' do
+          let(:source) { group }
+        end
+      end
+
+      context 'and :force param is true' do
+        let(:params) { { user_id: access_requester.id, force: true } }
+
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { project }
+        end
+
+        it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+          let(:source) { group }
+        end
+      end
+    end
+
+    context 'when current user cannot approve access request to the project' do
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { group }
+      end
+    end
+
+    context 'when current user can approve access request to the project' do
+      before do
+        project.team << [user, :master]
+        group.add_owner(user)
+      end
+
+      it_behaves_like 'a service approving an access request' do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a service approving an access request' do
+        let(:source) { group }
+      end
+
+      context 'when given a :id' do
+        let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
+
+        it_behaves_like 'a service approving an access request' do
+          let(:source) { project }
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0670ac2faa2b943dac8fdd65e396cf7ec49dd5a8
--- /dev/null
+++ b/spec/services/members/create_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Members::CreateService, services: true do
+  let(:project) { create(:empty_project) }
+  let(:user) { create(:user) }
+  let(:project_user) { create(:user) }
+
+  before { project.team << [user, :master] }
+
+  it 'adds user to members' do
+    params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
+    result = described_class.new(project, user, params).execute
+
+    expect(result).to be_truthy
+    expect(project.users).to include project_user
+  end
+
+  it 'adds no user to members' do
+    params = { user_ids: '', access_level: Gitlab::Access::GUEST }
+    result = described_class.new(project, user, params).execute
+
+    expect(result).to be_falsey
+    expect(project.users).not_to include project_user
+  end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 2395445e7fddcb1372b5010dea71680d712f174b..9995f3488af78d6e4b7ea0639836d95bbe814868 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -2,70 +2,111 @@ require 'spec_helper'
 
 describe Members::DestroyService, services: true do
   let(:user) { create(:user) }
-  let(:project) { create(:project) }
-  let!(:member) { create(:project_member, source: project) }
+  let(:member_user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:group) { create(:group, :public) }
 
-  context 'when member is nil' do
-    before do
-      project.team << [user, :developer]
+  shared_examples 'a service raising ActiveRecord::RecordNotFound' do
+    it 'raises ActiveRecord::RecordNotFound' do
+      expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
     end
+  end
 
-    it 'does not destroy the member' do
-      expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+  shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
     end
   end
 
-  context 'when current user cannot destroy the given member' do
-    before do
-      project.team << [user, :developer]
+  shared_examples 'a service destroying a member' do
+    it 'destroys the member' do
+      expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
+    end
+
+    context 'when the given member is an access requester' do
+      before do
+        source.members.find_by(user_id: member_user).destroy
+        source.request_access(member_user)
+      end
+      let(:access_requester) { source.requesters.find_by(user_id: member_user) }
+
+      it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
+
+      %i[requesters all].each do |scope|
+        context "and #{scope} scope is passed" do
+          it 'destroys the access requester' do
+            expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
+          end
+
+          it 'calls Member#after_decline_request' do
+            expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
+
+            described_class.new(source, user, params).execute(scope)
+          end
+
+          context 'when current user is the member' do
+            it 'does not call Member#after_decline_request' do
+              expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
+
+              described_class.new(source, member_user, params).execute(scope)
+            end
+          end
+        end
+      end
     end
+  end
+
+  context 'when no member are found' do
+    let(:params) { { user_id: 42 } }
 
-    it 'does not destroy the member' do
-      expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { project }
+    end
+
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { group }
     end
   end
 
-  context 'when current user can destroy the given member' do
+  context 'when a member is found' do
     before do
-      project.team << [user, :master]
+      project.team << [member_user, :developer]
+      group.add_developer(member_user)
     end
+    let(:params) { { user_id: member_user.id } }
 
-    it 'destroys the member' do
-      destroy_member(member, user)
+    context 'when current user cannot destroy the given member' do
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { project }
+      end
 
-      expect(member).to be_destroyed
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { group }
+      end
     end
 
-    context 'when the given member is a requester' do
+    context 'when current user can destroy the given member' do
       before do
-        member.update_column(:requested_at, Time.now)
+        project.team << [user, :master]
+        group.add_owner(user)
       end
 
-      it 'calls Member#after_decline_request' do
-        expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
-
-        destroy_member(member, user)
+      it_behaves_like 'a service destroying a member' do
+        let(:source) { project }
       end
 
-      context 'when current user is the member' do
-        it 'does not call Member#after_decline_request' do
-          expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
-
-          destroy_member(member, member.user)
-        end
+      it_behaves_like 'a service destroying a member' do
+        let(:source) { group }
       end
 
-      context 'when current user is the member and ' do
-        it 'does not call Member#after_decline_request' do
-          expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+      context 'when given a :id' do
+        let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
 
-          destroy_member(member, member.user)
+        it 'destroys the member' do
+          expect { described_class.new(project, user, params).execute }.
+            to change { project.members.count }.by(-1)
         end
       end
     end
   end
-
-  def destroy_member(member, user)
-    Members::DestroyService.new(member, user).execute
-  end
 end
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0d2d5f03199cbbcedc51a140521e64f63c5e7e01
--- /dev/null
+++ b/spec/services/members/request_access_service_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Members::RequestAccessService, services: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :private) }
+  let(:group) { create(:group, :private) }
+
+  shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+
+  shared_examples 'a service creating a access request' do
+    it 'succeeds' do
+      expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1)
+    end
+
+    it 'returns a <Source>Member' do
+      member = described_class.new(source, user).execute
+
+      expect(member).to be_a "#{source.class}Member".constantize
+      expect(member.requested_at).to be_present
+    end
+  end
+
+  context 'when source is nil' do
+    it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+      let(:source) { nil }
+    end
+  end
+
+  context 'when current user cannot request access to the project' do
+    it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+      let(:source) { project }
+    end
+
+    it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+      let(:source) { group }
+    end
+  end
+
+  context 'when current user can request access to the project' do
+    before do
+      project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+      group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+    end
+
+    it_behaves_like 'a service creating a access request' do
+      let(:source) { project }
+    end
+
+    it_behaves_like 'a service creating a access request' do
+      let(:source) { group }
+    end
+  end
+end
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5034b6ef33f14780ca0771686b2fd812cc1916df
--- /dev/null
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe MergeRequests::AssignIssuesService, services: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:issue) { create(:issue, project: project) }
+  let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") }
+  let(:service) { described_class.new(project, user, merge_request: merge_request) }
+
+  before do
+    project.team << [user, :developer]
+  end
+
+  it 'finds unassigned issues fixed in merge request' do
+    expect(service.assignable_issues.map(&:id)).to include(issue.id)
+  end
+
+  it 'ignores issues already assigned to any user' do
+    issue.update!(assignee: create(:user))
+
+    expect(service.assignable_issues).to be_empty
+  end
+
+  it 'ignores issues the user cannot update assignee on' do
+    project.team.truncate
+
+    expect(service.assignable_issues).to be_empty
+  end
+
+  it 'ignores all issues unless current_user is merge_request.author' do
+    merge_request.update!(author: create(:user))
+
+    expect(service.assignable_issues).to be_empty
+  end
+
+  it 'accepts precomputed data for closes_issues' do
+    issue2 = create(:issue, project: project)
+    service2 = described_class.new(project,
+                                   user,
+                                   merge_request: merge_request,
+                                   closes_issues: [issue, issue2])
+
+    expect(service2.assignable_issues.count).to eq 2
+  end
+
+  it 'assigns these to the merge request owner' do
+    expect { service.execute }.to change { issue.reload.assignee }.to(user)
+  end
+
+  it 'ignores external issues' do
+    external_issue = ExternalIssue.new('JIRA-123', project)
+    service = described_class.new(
+      project,
+      user,
+      merge_request: merge_request,
+      closes_issues: [external_issue]
+    )
+
+    expect(service.assignable_issues.count).to eq 0
+  end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 232508cda23bc4f4e6cc7b1c040c2c3248a61bd7..3f5df049ea2aeb4afbebec55bcc872e2ecd66a52 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do
 
   before do
     allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
+    allow(project).to receive(:commit).and_return(commit_1)
+    allow(project).to receive(:commit).and_return(commit_2)
   end
 
   describe 'execute' do
@@ -52,12 +54,28 @@ describe MergeRequests::BuildService, services: true do
       end
     end
 
-    context 'no commits in the diff' do
-      let(:commits) { [] }
+    context 'same source and target branch' do
+      let(:source_branch) { 'master' }
 
       it 'forbids the merge request from being created' do
         expect(merge_request.can_be_created).to eq(false)
       end
+
+      it 'adds an error message to the merge request' do
+        expect(merge_request.errors).to contain_exactly('You must select different branches')
+      end
+    end
+
+    context 'no commits in the diff' do
+      let(:commits) { [] }
+
+      it 'allows the merge request to be created' do
+        expect(merge_request.can_be_created).to eq(true)
+      end
+
+      it 'adds a WIP prefix to the merge request title' do
+        expect(merge_request.title).to eq('WIP: Feature branch')
+      end
     end
 
     context 'one commit in the diff' do
@@ -99,14 +117,14 @@ describe MergeRequests::BuildService, services: true do
         let(:source_branch) { "#{issue.iid}-fix-issue" }
 
         it 'appends "Closes #$issue-iid" to the description' do
-          expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}")
+          expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}")
         end
 
         context 'merge request already has a description set' do
           let(:description) { 'Merge request description' }
 
           it 'appends "Closes #$issue-iid" to the description' do
-            expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}")
+            expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}")
           end
         end
 
@@ -177,5 +195,52 @@ describe MergeRequests::BuildService, services: true do
         end
       end
     end
+
+    context 'source branch does not exist' do
+      before do
+        allow(project).to receive(:commit).with(source_branch).and_return(nil)
+        allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
+      end
+
+      it 'forbids the merge request from being created' do
+        expect(merge_request.can_be_created).to eq(false)
+      end
+
+      it 'adds an error message to the merge request' do
+        expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist')
+      end
+    end
+
+    context 'target branch does not exist' do
+      before do
+        allow(project).to receive(:commit).with(source_branch).and_return(commit_1)
+        allow(project).to receive(:commit).with(target_branch).and_return(nil)
+      end
+
+      it 'forbids the merge request from being created' do
+        expect(merge_request.can_be_created).to eq(false)
+      end
+
+      it 'adds an error message to the merge request' do
+        expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist')
+      end
+    end
+
+    context 'both source and target branches do not exist' do
+      before do
+        allow(project).to receive(:commit).and_return(nil)
+      end
+
+      it 'forbids the merge request from being created' do
+        expect(merge_request.can_be_created).to eq(false)
+      end
+
+      it 'adds both error messages to the merge request' do
+        expect(merge_request.errors).to contain_exactly(
+          'Source branch "feature-branch" does not exist',
+          'Target branch "master" does not exist'
+        )
+      end
+    end
   end
 end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 403533be5d9bd4e487f8971e64607faffe01c275..24c25e4350f9b3913a9e6a8025f069abd5c8a697 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
 describe MergeRequests::CloseService, services: true do
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
+  let(:guest) { create(:user) }
   let(:merge_request) { create(:merge_request, assignee: user2) }
   let(:project) { merge_request.project }
   let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
@@ -10,11 +11,12 @@ describe MergeRequests::CloseService, services: true do
   before do
     project.team << [user, :master]
     project.team << [user2, :developer]
+    project.team << [guest, :guest]
   end
 
   describe '#execute' do
     context 'valid params' do
-      let(:service) { MergeRequests::CloseService.new(project, user, {}) }
+      let(:service) { described_class.new(project, user, {}) }
 
       before do
         allow(service).to receive(:execute_hooks)
@@ -47,5 +49,17 @@ describe MergeRequests::CloseService, services: true do
         expect(todo.reload).to be_done
       end
     end
+
+    context 'current user is not authorized to close merge request' do
+      before do
+        perform_enqueued_jobs do
+          @merge_request = described_class.new(project, guest).execute(merge_request)
+        end
+      end
+
+      it 'does not close the merge request' do
+        expect(@merge_request).to be_open
+      end
+    end
   end
 end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index b84a580967ad92c95ea0499aa3645e9033e197cf..b81428890756d6d902c90fa12251b6a1bc63a6a1 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequests::CreateService, services: true do
         }
       end
 
-      let(:service) { MergeRequests::CreateService.new(project, user, opts) }
+      let(:service) { described_class.new(project, user, opts) }
 
       before do
         project.team << [user, :master]
@@ -74,5 +74,43 @@ describe MergeRequests::CreateService, services: true do
         end
       end
     end
+
+    it_behaves_like 'new issuable record that supports slash commands' do
+      let(:default_params) do
+        {
+          source_branch: 'feature',
+          target_branch: 'master'
+        }
+      end
+    end
+
+    context 'while saving references to issues that the created merge request closes' do
+      let(:first_issue) { create(:issue, project: project) }
+      let(:second_issue) { create(:issue, project: project) }
+
+      let(:opts) do
+        {
+          title: 'Awesome merge_request',
+          source_branch: 'feature',
+          target_branch: 'master',
+          force_remove_source_branch: '1'
+        }
+      end
+
+      before do
+        project.team << [user, :master]
+        project.team << [assignee, :developer]
+      end
+
+      it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+        issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}")
+        service = described_class.new(project, user, issue_closing_opts)
+        allow(service).to receive(:execute_hooks)
+        merge_request = service.execute
+
+        issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+        expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+      end
+    end
   end
 end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 8a4b76367e32edce78979525fb064e2abf4197a1..3a71776e81f61ab99cd4a36aa5895017911d1619 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do
       let(:changes) { new_branch_changes }
 
       before do
-        project.merge_requests_enabled = false
+        project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED)
       end
 
       it_behaves_like 'no_merge_request_url'
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index c4b874682751c550ab3ce83e6d90b25ce9f15046..807f89e80b76736270ceda59138d48f47164bd11 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
   describe '#execute' do
     it 'retrieves the diff files to cache the highlighted result' do
       merge_request = create(:merge_request)
-      cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options]
+      cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options]
 
       expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
       expect(Rails.cache).to receive(:write).with(cache_key, anything)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 159f6817e8d9bbd9ade7d6a845a30dbfc21dcefb..f93d7732a9a08987416b7b33a9aa754b531495a0 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,81 @@ describe MergeRequests::MergeService, services: true do
       end
     end
 
+    context 'closes related issues' do
+      let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+      before do
+        allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
+      end
+
+      it 'closes GitLab issue tracker issues' do
+        issue  = create :issue, project: project
+        commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
+        allow(merge_request).to receive(:commits).and_return([commit])
+
+        service.execute(merge_request)
+
+        expect(issue.reload.closed?).to be_truthy
+      end
+
+      context 'with JIRA integration' do
+        include JiraServiceHelper
+
+        let(:jira_tracker) { project.create_jira_service }
+
+        before do
+          project.update_attributes!(has_external_issue_tracker: true)
+          jira_service_settings
+        end
+
+        it 'closes issues on JIRA issue tracker' do
+          jira_issue = ExternalIssue.new('JIRA-123', project)
+          commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+          allow(merge_request).to receive(:commits).and_return([commit])
+
+          expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+
+          service.execute(merge_request)
+        end
+
+        context "wrong issue markdown" do
+          it 'does not close issues on JIRA issue tracker' do
+            jira_issue = ExternalIssue.new('#123', project)
+            commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+            allow(merge_request).to receive(:commits).and_return([commit])
+
+            expect_any_instance_of(JiraService).not_to receive(:close_issue)
+
+            service.execute(merge_request)
+          end
+        end
+      end
+    end
+
+    context 'closes related todos' do
+      let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+      let(:project) { merge_request.project }
+      let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
+      let!(:todo) do
+        create(:todo, :assigned,
+          project: project,
+          author: user,
+          user: user,
+          target: merge_request)
+      end
+
+      before do
+        allow(service).to receive(:execute_hooks)
+
+        perform_enqueued_jobs do
+          service.execute(merge_request)
+          todo.reload
+        end
+      end
+
+      it { expect(todo).to be_done }
+    end
+
     context 'remove source branch by author' do
       let(:service) do
         merge_request.merge_params['force_remove_source_branch'] = '1'
@@ -57,13 +132,13 @@ describe MergeRequests::MergeService, services: true do
       let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
 
       it 'saves error if there is an exception' do
-        allow(service).to receive(:repository).and_raise("error")
+        allow(service).to receive(:repository).and_raise("error message")
 
         allow(service).to receive(:execute_hooks)
 
         service.execute(merge_request)
 
-        expect(merge_request.merge_error).to eq("Something went wrong during merge")
+        expect(merge_request.merge_error).to eq("Something went wrong during merge: error message")
       end
 
       it 'saves error if there is an PreReceiveError exception' do
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 520e906b21f357e9b8c909a5d7160f743a819a63..1f90efdbd6af69f2dc36bf8393cc2a670479bcb0 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -58,61 +58,83 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
   end
 
   describe "#trigger" do
-    context 'build with ref' do
-      let(:build)     { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+    let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
+    let(:merge_request_head) do
+      project.commit(mr_merge_if_green_enabled.source_branch).id
+    end
 
-      it "merges all merge requests with merge when build succeeds enabled" do
-        allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
-        allow(pipeline).to receive(:success?).and_return(true)
+    context 'when triggered by pipeline with valid ref and sha' do
+      let(:triggering_pipeline) do
+        create(:ci_pipeline, project: project, ref: merge_request_ref,
+                             sha: merge_request_head, status: 'success')
+      end
 
+      it "merges all merge requests with merge when build succeeds enabled" do
         expect(MergeWorker).to receive(:perform_async)
-        service.trigger(build)
+        service.trigger(triggering_pipeline)
       end
     end
 
-    context 'triggered by an old build' do
-      let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
-      let(:build)     { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
-
-      it "merges all merge requests with merge when build succeeds enabled" do
-        allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
-        allow(pipeline).to receive(:success?).and_return(true)
-        allow(old_build).to receive(:sha).and_return('1234abcdef')
+    context 'when triggered by an old pipeline' do
+      let(:old_pipeline) do
+        create(:ci_pipeline, project: project, ref: merge_request_ref,
+                             sha: '1234abcdef', status: 'success')
+      end
 
+      it 'it does not merge merge request' do
         expect(MergeWorker).not_to receive(:perform_async)
-        service.trigger(old_build)
+        service.trigger(old_pipeline)
       end
     end
 
-    context 'commit status without ref' do
-      let(:commit_status) { create(:generic_commit_status, status: 'success') }
-
-      before { mr_merge_if_green_enabled }
-
-      it "doesn't merge a requests for status on other branch" do
-        allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
+    context 'when triggered by pipeline from a different branch' do
+      let(:unrelated_pipeline) do
+        create(:ci_pipeline, project: project, ref: 'feature',
+                             sha: merge_request_head, status: 'success')
+      end
 
+      it 'does not merge request' do
         expect(MergeWorker).not_to receive(:perform_async)
-        service.trigger(commit_status)
+        service.trigger(unrelated_pipeline)
       end
+    end
+  end
 
-      it 'discovers branches and merges all merge requests when status is success' do
-        allow(project.repository).to receive(:branch_names_contains).
-          with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch])
-        allow(pipeline).to receive(:success?).and_return(true)
-        allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
-        allow(pipeline).to receive(:success?).and_return(true)
+  describe "#cancel" do
+    before do
+      service.cancel(mr_merge_if_green_enabled)
+    end
 
-        expect(MergeWorker).to receive(:perform_async)
-        service.trigger(commit_status)
-      end
+    it "resets all the merge_when_build_succeeds params" do
+      expect(mr_merge_if_green_enabled.merge_when_build_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
 
-    context 'properly handles multiple stages' do
+    it 'Posts a system note' do
+      note = mr_merge_if_green_enabled.notes.last
+      expect(note.note).to include 'Canceled the automatic merge'
+    end
+  end
+
+  describe 'pipeline integration' do
+    context 'when there are multiple stages in the pipeline' do
       let(:ref) { mr_merge_if_green_enabled.source_branch }
-      let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
-      let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
-      let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+      let(:sha) { project.commit(ref).id }
+
+      let(:pipeline) do
+        create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
+      end
+
+      let!(:build) do
+        create(:ci_build, :created, pipeline: pipeline, ref: ref,
+                                    name: 'build', stage: 'build')
+      end
+
+      let!(:test) do
+        create(:ci_build, :created, pipeline: pipeline, ref: ref,
+                                    name: 'test', stage: 'test')
+      end
 
       before do
         # This behavior of MergeRequest: we instantiate a new object
@@ -121,34 +143,21 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
         end
       end
 
-      it "doesn't merge if some stages failed" do
+      it "doesn't merge if any of stages failed" do
         expect(MergeWorker).not_to receive(:perform_async)
+
         build.success
+        test.reload
         test.drop
       end
 
-      it 'merge when all stages succeeded' do
+      it 'merges when all stages succeeded' do
         expect(MergeWorker).to receive(:perform_async)
+
         build.success
+        test.reload
         test.success
       end
     end
   end
-
-  describe "#cancel" do
-    before 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
-      expect(mr_merge_if_green_enabled.merge_params).to eq({})
-      expect(mr_merge_if_green_enabled.merge_user).to be nil
-    end
-
-    it 'Posts a system note' do
-      note = mr_merge_if_green_enabled.notes.last
-      expect(note.note).to include 'Canceled the automatic merge'
-    end
-  end
 end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fff86480c6d7771c6da31841523b7679e4081b76..e515bc9f89c2dabc066b3c9b776635a070026cca 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -62,7 +62,8 @@ describe MergeRequests::RefreshService, services: true do
 
       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.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 }
@@ -79,8 +80,8 @@ describe MergeRequests::RefreshService, services: true do
       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('changed to merged') }
-      it { expect(@build_failed_todo).to be_pending }
-      it { expect(@fork_build_failed_todo).to be_pending }
+      it { expect(@build_failed_todo).to be_done }
+      it { expect(@fork_build_failed_todo).to be_done }
     end
 
     context 'manual merge of source branch' do
@@ -99,8 +100,8 @@ describe MergeRequests::RefreshService, services: true do
       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('changed to merged') }
-      it { expect(@build_failed_todo).to be_pending }
-      it { expect(@fork_build_failed_todo).to be_pending }
+      it { expect(@build_failed_todo).to be_done }
+      it { expect(@fork_build_failed_todo).to be_done }
     end
 
     context 'push to fork repo source branch' do
@@ -118,7 +119,7 @@ describe MergeRequests::RefreshService, services: true do
 
       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 4 commits') }
+      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 }
@@ -149,8 +150,8 @@ describe MergeRequests::RefreshService, services: true do
       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_pending }
-      it { expect(@fork_build_failed_todo).to be_pending }
+      it { expect(@build_failed_todo).to be_done }
+      it { expect(@fork_build_failed_todo).to be_done }
     end
 
     context 'push new branch that exists in a merge request' do
@@ -169,11 +170,63 @@ describe MergeRequests::RefreshService, services: true do
 
         notes = @fork_merge_request.notes.reorder(:created_at).map(&:note)
         expect(notes[0]).to include('Restored source branch `master`')
-        expect(notes[1]).to include('Added 4 commits')
+        expect(notes[1]).to include('Added 28 commits')
         expect(@fork_merge_request).to be_open
       end
     end
 
+    context 'merge request metrics' do
+      let(:issue) { create :issue, project: @project }
+      let(:commit_author) { create :user }
+      let(:commit) { project.commit }
+
+      before do
+        project.team << [commit_author, :developer]
+        project.team << [user, :developer]
+
+        allow(commit).to receive_messages(
+          safe_message: "Closes #{issue.to_reference}",
+          references: [issue],
+          author_name: commit_author.name,
+          author_email: commit_author.email,
+          committed_date: Time.now
+        )
+
+        allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit])
+      end
+
+      context 'when the merge request is sourced from the same project' do
+        it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+          merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
+          refresh_service = service.new(@project, @user)
+          allow(refresh_service).to receive(:execute_hooks)
+          refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+          issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+          expect(issue_ids).to eq([issue.id])
+        end
+      end
+
+      context 'when the merge request is sourced from a different project' do
+        it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+          forked_project = create(:project)
+          create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+
+          merge_request = create(:merge_request,
+                                 target_branch: 'master',
+                                 source_branch: 'feature',
+                                 target_project: @project,
+                                 source_project: forked_project)
+          refresh_service = service.new(@project, @user)
+          allow(refresh_service).to receive(:execute_hooks)
+          refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+          issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+          expect(issue_ids).to eq([issue.id])
+        end
+      end
+    end
+
     def reload_mrs
       @merge_request.reload
       @fork_merge_request.reload
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 3419b8bf5e6acaec140c0064cdff360d53281fd1..af7424a76a98c2c7231d07edacc7fd0afb40c83b 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -3,22 +3,23 @@ require 'spec_helper'
 describe MergeRequests::ReopenService, services: true do
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
-  let(:merge_request) { create(:merge_request, assignee: user2) }
+  let(:guest) { create(:user) }
+  let(:merge_request) { create(:merge_request, :closed, assignee: user2) }
   let(:project) { merge_request.project }
 
   before do
     project.team << [user, :master]
     project.team << [user2, :developer]
+    project.team << [guest, :guest]
   end
 
   describe '#execute' do
     context 'valid params' do
-      let(:service) { MergeRequests::ReopenService.new(project, user, {}) }
+      let(:service) { described_class.new(project, user, {}) }
 
       before do
         allow(service).to receive(:execute_hooks)
 
-        merge_request.state = :closed
         perform_enqueued_jobs do
           service.execute(merge_request)
         end
@@ -43,5 +44,17 @@ describe MergeRequests::ReopenService, services: true do
         expect(note.note).to include 'Status changed to reopened'
       end
     end
+
+    context 'current user is not authorized to reopen merge request' do
+      before do
+        perform_enqueued_jobs do
+          @merge_request = described_class.new(project, guest).execute(merge_request)
+        end
+      end
+
+      it 'does not reopen the merge request' do
+        expect(@merge_request).to be_closed
+      end
+    end
   end
 end
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..388abb6a0dfc3045f0b82d7bdc0450a9f89fb54a
--- /dev/null
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -0,0 +1,208 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolveService do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+
+  let(:fork_project) do
+    create(:forked_project_with_submodules) do |fork_project|
+      fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+      fork_project.save
+    end
+  end
+
+  let(:merge_request) do
+    create(:merge_request,
+           source_branch: 'conflict-resolvable', source_project: project,
+           target_branch: 'conflict-start')
+  end
+
+  let(:merge_request_from_fork) do
+    create(:merge_request,
+           source_branch: 'conflict-resolvable-fork', source_project: fork_project,
+           target_branch: 'conflict-start', target_project: project)
+  end
+
+  describe '#execute' do
+    context 'with section params' do
+      let(:params) do
+        {
+          files: [
+            {
+              old_path: 'files/ruby/popen.rb',
+              new_path: 'files/ruby/popen.rb',
+              sections: {
+                '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
+              }
+            }, {
+              old_path: 'files/ruby/regex.rb',
+              new_path: 'files/ruby/regex.rb',
+              sections: {
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+              }
+            }
+          ],
+          commit_message: 'This is a commit message!'
+        }
+      end
+
+      context 'when the source and target project are the same' do
+        before do
+          MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+        end
+
+        it 'creates a commit with the message' do
+          expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+        end
+
+        it 'creates a commit with the correct parents' do
+          expect(merge_request.source_branch_head.parents.map(&:id)).
+            to eq(['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(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+        end
+
+        before do
+          MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork)
+        end
+
+        it 'creates a commit with the message' do
+          expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
+        end
+
+        it 'creates a commit with the correct parents' do
+          expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
+            to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
+                   target_head])
+        end
+      end
+    end
+
+    context 'with content and sections params' do
+      let(:popen_content) { "class Popen\nend" }
+
+      let(:params) do
+        {
+          files: [
+            {
+              old_path: 'files/ruby/popen.rb',
+              new_path: 'files/ruby/popen.rb',
+              content: popen_content
+            }, {
+              old_path: 'files/ruby/regex.rb',
+              new_path: 'files/ruby/regex.rb',
+              sections: {
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+                '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+              }
+            }
+          ],
+          commit_message: 'This is a commit message!'
+        }
+      end
+
+      before do
+        MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+      end
+
+      it 'creates a commit with the message' do
+        expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+      end
+
+      it 'creates a commit with the correct parents' do
+        expect(merge_request.source_branch_head.parents.map(&:id)).
+          to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
+                 '824be604a34828eb682305f0d963056cfac87b2d'])
+      end
+
+      it 'sets the content to the content given' do
+        blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
+                                                               'files/ruby/popen.rb')
+
+        expect(blob.data).to eq(popen_content)
+      end
+    end
+
+    context 'when a resolution section is missing' do
+      let(:invalid_params) do
+        {
+          files: [
+            {
+              old_path: 'files/ruby/popen.rb',
+              new_path: 'files/ruby/popen.rb',
+              content: ''
+            }, {
+              old_path: 'files/ruby/regex.rb',
+              new_path: 'files/ruby/regex.rb',
+              sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' }
+            }
+          ],
+          commit_message: 'This is a commit message!'
+        }
+      end
+
+      let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+      it 'raises a MissingResolution error' do
+        expect { service.execute(merge_request) }.
+          to raise_error(Gitlab::Conflict::File::MissingResolution)
+      end
+    end
+
+    context 'when the content of a file is unchanged' do
+      let(:invalid_params) do
+        {
+          files: [
+            {
+              old_path: 'files/ruby/popen.rb',
+              new_path: 'files/ruby/popen.rb',
+              content: ''
+            }, {
+              old_path: 'files/ruby/regex.rb',
+              new_path: 'files/ruby/regex.rb',
+              content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+            }
+          ],
+          commit_message: 'This is a commit message!'
+        }
+      end
+
+      let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+      it 'raises a MissingResolution error' do
+        expect { service.execute(merge_request) }.
+          to raise_error(Gitlab::Conflict::File::MissingResolution)
+      end
+    end
+
+    context 'when a file is missing' do
+      let(:invalid_params) do
+        {
+          files: [
+            {
+              old_path: 'files/ruby/popen.rb',
+              new_path: 'files/ruby/popen.rb',
+              content: ''
+            }
+          ],
+          commit_message: 'This is a commit message!'
+        }
+      end
+
+      let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+      it 'raises a MissingFiles error' do
+        expect { service.execute(merge_request) }.
+          to raise_error(MergeRequests::ResolveService::MissingFiles)
+      end
+    end
+  end
+end
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7ddd812e513ad5d9439e77da1406972772437527
--- /dev/null
+++ b/spec/services/merge_requests/resolved_discussion_notification_service.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolvedDiscussionNotificationService, services: true do
+  let(:merge_request) { create(:merge_request) }
+  let(:user) { create(:user) }
+  let(:project) { merge_request.project }
+  subject { described_class.new(project, user) }
+
+  describe "#execute" do
+    context "when not all discussions are resolved" do
+      before do
+        allow(merge_request).to receive(:discussions_resolved?).and_return(false)
+      end
+
+      it "doesn't add a system note" do
+        expect(SystemNoteService).not_to receive(:resolve_all_discussions)
+
+        subject.execute(merge_request)
+      end
+
+      it "doesn't send a notification email" do
+        expect_any_instance_of(NotificationService).not_to receive(:resolve_all_discussions)
+
+        subject.execute(merge_request)
+      end
+    end
+
+    context "when all discussions are resolved" do
+      before do
+        allow(merge_request).to receive(:discussions_resolved?).and_return(true)
+      end
+
+      it "adds a system note" do
+        expect(SystemNoteService).to receive(:resolve_all_discussions).with(merge_request, project, user)
+
+        subject.execute(merge_request)
+      end
+
+      it "sends a notification email" do
+        expect_any_instance_of(NotificationService).to receive(:resolve_all_discussions).with(merge_request, user)
+
+        subject.execute(merge_request)
+      end
+    end
+  end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 283a336afd9346ceed2f6e772b966baae5214115..2433a7dad06a201f86c9254982d208eda472d892 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -17,6 +17,7 @@ describe MergeRequests::UpdateService, services: true do
   before do
     project.team << [user, :master]
     project.team << [user2, :developer]
+    project.team << [user3, :developer]
   end
 
   describe 'execute' do
@@ -104,6 +105,18 @@ describe MergeRequests::UpdateService, services: true do
         expect(note).not_to be_nil
         expect(note.note).to eq 'Target branch changed from `master` to `target`'
       end
+
+      context 'when not including source branch removal options' do
+        before do
+          opts.delete(:force_remove_source_branch)
+        end
+
+        it 'maintains the original options' do
+          update_merge_request(opts)
+
+          expect(@merge_request.merge_params["force_remove_source_branch"]).to eq("1")
+        end
+      end
     end
 
     context 'todos' do
@@ -188,6 +201,11 @@ describe MergeRequests::UpdateService, services: true do
       let!(:non_subscriber) { create(:user) }
       let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
 
+      before do
+        project.team << [non_subscriber, :developer]
+        project.team << [subscriber, :developer]
+      end
+
       it 'sends notifications for subscribers of newly added labels' do
         opts = { label_ids: [label.id] }
 
@@ -226,6 +244,11 @@ describe MergeRequests::UpdateService, services: true do
       end
     end
 
+    context 'updating mentions' do
+      let(:mentionable) { merge_request }
+      include_examples 'updating mentions', MergeRequests::UpdateService
+    end
+
     context 'when MergeRequest has tasks' do
       before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
 
@@ -258,5 +281,42 @@ describe MergeRequests::UpdateService, services: true do
         end
       end
     end
+
+    context 'while saving references to issues that the updated merge request closes' do
+      let(:first_issue) { create(:issue, project: project) }
+      let(:second_issue) { create(:issue, project: project) }
+
+      it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+        issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" }
+        service = described_class.new(project, user, issue_closing_opts)
+        allow(service).to receive(:execute_hooks)
+        service.execute(merge_request)
+
+        issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+        expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+      end
+
+      it 'removes `MergeRequestsClosingIssues` records when issues are not closed anymore' do
+        opts = {
+          title: 'Awesome merge_request',
+          description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}",
+          source_branch: 'feature',
+          target_branch: 'master',
+          force_remove_source_branch: '1'
+        }
+
+        merge_request = MergeRequests::CreateService.new(project, user, opts).execute
+
+        issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+        expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+
+        service = described_class.new(project, user, description: "not closing any issues")
+        allow(service).to receive(:execute_hooks)
+        service.execute(merge_request.reload)
+
+        issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+        expect(issue_ids).to be_empty
+      end
+    end
   end
 end
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 5d400299be0ba9a13e545351a8df958fcdbf2ad3..92b84308f73580c4fbc0c6419c0a4b2c8881ac5c 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -18,7 +18,7 @@ describe Milestones::CloseService, services: true do
     it { expect(milestone).to be_closed }
 
     describe :event do
-      let(:event) { Event.first }
+      let(:event) { Event.recent.first }
 
       it { expect(event.milestone).to be_truthy }
       it { expect(event.target).to eq(milestone) }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 32753e84b314c7e7391e54ea9fc9846c79ca6c46..93885c84dc3b9dcc0fa53d71a433a65f357179ae 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -4,22 +4,36 @@ describe Notes::CreateService, services: true do
   let(:project) { create(:empty_project) }
   let(:issue) { create(:issue, project: project) }
   let(:user) { create(:user) }
+  let(:opts) do
+    { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id }
+  end
 
   describe '#execute' do
+    before do
+      project.team << [user, :master]
+    end
+
     context "valid params" do
       before do
-        project.team << [user, :master]
-        opts = {
-          note: 'Awesome comment',
-          noteable_type: 'Issue',
-          noteable_id: issue.id
-        }
-
         @note = Notes::CreateService.new(project, user, opts).execute
       end
 
       it { expect(@note).to be_valid }
-      it { expect(@note.note).to eq('Awesome comment') }
+      it { expect(@note.note).to eq(opts[:note]) }
+    end
+
+    describe 'note with commands' do
+      describe '/close, /label, /assign & /milestone' do
+        let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) }
+
+        it 'saves the note and does not alter the note text' do
+          expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original
+
+          note = described_class.new(project, user, opts.merge(note: note_text)).execute
+
+          expect(note.note).to eq "HELLO\nWORLD"
+        end
+      end
     end
   end
 
@@ -42,7 +56,7 @@ describe Notes::CreateService, services: true do
 
     it "creates regular note if emoji name is invalid" do
       opts = {
-        note: ':smile: moretext: ',
+        note: ':smile: moretext:',
         noteable_type: 'Issue',
         noteable_id: issue.id
       }
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d1099884a02480a7f198eeed6a2b8e0fd2ce64fb
--- /dev/null
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -0,0 +1,209 @@
+require 'spec_helper'
+
+describe Notes::SlashCommandsService, services: true do
+  shared_context 'note on noteable' do
+    let(:project) { create(:empty_project) }
+    let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+    let(:assignee) { create(:user) }
+  end
+
+  shared_examples 'note on noteable that does not support slash commands' do
+    include_context 'note on noteable'
+
+    before do
+      note.note = note_text
+    end
+
+    describe 'note with only command' do
+      describe '/close, /label, /assign & /milestone' do
+        let(:note_text) { %(/close\n/assign @#{assignee.username}") }
+
+        it 'saves the note and does not alter the note text' do
+          content, command_params = service.extract_commands(note)
+
+          expect(content).to eq note_text
+          expect(command_params).to be_empty
+        end
+      end
+    end
+
+    describe 'note with command & text' do
+      describe '/close, /label, /assign & /milestone' do
+        let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) }
+
+        it 'saves the note and does not alter the note text' do
+          content, command_params = service.extract_commands(note)
+
+          expect(content).to eq note_text
+          expect(command_params).to be_empty
+        end
+      end
+    end
+  end
+
+  shared_examples 'note on noteable that supports slash commands' do
+    include_context 'note on noteable'
+
+    before do
+      note.note = note_text
+    end
+
+    let!(:milestone) { create(:milestone, project: project) }
+    let!(:labels) { create_pair(:label, project: project) }
+
+    describe 'note with only command' do
+      describe '/close, /label, /assign & /milestone' do
+        let(:note_text) do
+          %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+        end
+
+        it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
+          content, command_params = service.extract_commands(note)
+          service.execute(command_params, note)
+
+          expect(content).to eq ''
+          expect(note.noteable).to be_closed
+          expect(note.noteable.labels).to match_array(labels)
+          expect(note.noteable.assignee).to eq(assignee)
+          expect(note.noteable.milestone).to eq(milestone)
+        end
+      end
+
+      describe '/reopen' do
+        before do
+          note.noteable.close!
+          expect(note.noteable).to be_closed
+        end
+        let(:note_text) { '/reopen' }
+
+        it 'opens the noteable, and leave no note' do
+          content, command_params = service.extract_commands(note)
+          service.execute(command_params, note)
+
+          expect(content).to eq ''
+          expect(note.noteable).to be_open
+        end
+      end
+    end
+
+    describe 'note with command & text' do
+      describe '/close, /label, /assign & /milestone' do
+        let(:note_text) do
+          %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD)
+        end
+
+        it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
+          content, command_params = service.extract_commands(note)
+          service.execute(command_params, note)
+
+          expect(content).to eq "HELLO\nWORLD"
+          expect(note.noteable).to be_closed
+          expect(note.noteable.labels).to match_array(labels)
+          expect(note.noteable.assignee).to eq(assignee)
+          expect(note.noteable.milestone).to eq(milestone)
+        end
+      end
+
+      describe '/reopen' do
+        before do
+          note.noteable.close
+          expect(note.noteable).to be_closed
+        end
+        let(:note_text) { "HELLO\n/reopen\nWORLD" }
+
+        it 'opens the noteable' do
+          content, command_params = service.extract_commands(note)
+          service.execute(command_params, note)
+
+          expect(content).to eq "HELLO\nWORLD"
+          expect(note.noteable).to be_open
+        end
+      end
+    end
+  end
+
+  describe '.noteable_update_service' do
+    include_context 'note on noteable'
+
+    it 'returns Issues::UpdateService for a note on an issue' do
+      note = create(:note_on_issue, project: project)
+
+      expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService)
+    end
+
+    it 'returns Issues::UpdateService for a note on a merge request' do
+      note = create(:note_on_merge_request, project: project)
+
+      expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService)
+    end
+
+    it 'returns nil for a note on a commit' do
+      note = create(:note_on_commit, project: project)
+
+      expect(described_class.noteable_update_service(note)).to be_nil
+    end
+  end
+
+  describe '.supported?' do
+    include_context 'note on noteable'
+
+    let(:note) { create(:note_on_issue, project: project) }
+
+    context 'with no current_user' do
+      it 'returns false' do
+        expect(described_class.supported?(note, nil)).to be_falsy
+      end
+    end
+
+    context 'when current_user cannot update the noteable' do
+      it 'returns false' do
+        user = create(:user)
+
+        expect(described_class.supported?(note, user)).to be_falsy
+      end
+    end
+
+    context 'when current_user can update the noteable' do
+      it 'returns true' do
+        expect(described_class.supported?(note, master)).to be_truthy
+      end
+
+      context 'with a note on a commit' do
+        let(:note) { create(:note_on_commit, project: project) }
+
+        it 'returns false' do
+          expect(described_class.supported?(note, nil)).to be_falsy
+        end
+      end
+    end
+  end
+
+  describe '#supported?' do
+    include_context 'note on noteable'
+
+    it 'delegates to the class method' do
+      service = described_class.new(project, master)
+      note = create(:note_on_issue, project: project)
+
+      expect(described_class).to receive(:supported?).with(note, master)
+
+      service.supported?(note)
+    end
+  end
+
+  describe '#execute' do
+    let(:service) { described_class.new(project, master) }
+
+    it_behaves_like 'note on noteable that supports slash commands' do
+      let(:note) { build(:note_on_issue, project: project) }
+    end
+
+    it_behaves_like 'note on noteable that supports slash commands' do
+      let(:note) { build(:note_on_merge_request, project: project) }
+    end
+
+    it_behaves_like 'note on noteable that does not support slash commands' do
+      let(:note) { build(:note_on_commit, project: project) }
+    end
+  end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 92b441c28caa4dc8b4556f5b01ce6330f2528114..8ce35354c22fe1e34fd76fab5ab25677f4229a4b 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -9,6 +9,28 @@ describe NotificationService, services: true do
     end
   end
 
+  shared_examples 'notifications for new mentions' do
+    def send_notifications(*new_mentions)
+      reset_delivered_emails!
+      notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+    end
+
+    it 'sends no emails when no new mentions are present' do
+      send_notifications
+      should_not_email_anyone
+    end
+
+    it 'emails new mentions with a watch level higher than participant' do
+      send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global)
+      should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global)
+    end
+
+    it 'does not email new mentions with a watch level equal to or less than participant' do
+      send_notifications(@u_participating, @u_mentioned)
+      should_not_email_anyone
+    end
+  end
+
   describe 'Keys' do
     describe '#new_key' do
       let!(:key) { create(:personal_key) }
@@ -57,7 +79,7 @@ describe NotificationService, services: true do
           # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
           expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.new_note(note)
 
@@ -89,7 +111,7 @@ describe NotificationService, services: true do
         context 'participating' do
           context 'by note' do
             before do
-              ActionMailer::Base.deliveries.clear
+              reset_delivered_emails!
               note.author = @u_lazy_participant
               note.save
               notification.new_note(note)
@@ -112,7 +134,7 @@ describe NotificationService, services: true do
           @u_watcher.notification_settings_for(note.project).participating!
           @u_watcher.notification_settings_for(note.project.group).global!
           update_custom_notification(:new_note, @u_custom_global)
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
         end
 
         it do
@@ -151,7 +173,7 @@ describe NotificationService, services: true do
 
         expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
 
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
 
         notification.new_note(note)
 
@@ -174,7 +196,7 @@ describe NotificationService, services: true do
       before do
         build_team(note.project)
         note.project.team << [note.author, :master]
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       describe '#new_note' do
@@ -216,7 +238,7 @@ describe NotificationService, services: true do
       before do
         build_team(note.project)
         note.project.team << [note.author, :master]
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       describe '#new_note' do
@@ -251,7 +273,7 @@ describe NotificationService, services: true do
 
       before do
         build_team(note.project)
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
         allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
         update_custom_notification(:new_note, @u_guest_custom, project)
         update_custom_notification(:new_note, @u_custom_global)
@@ -309,7 +331,7 @@ describe NotificationService, services: true do
       describe '#new_note' do
         it "records sent notifications" do
           # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
-          expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original
+          expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
 
           notification.new_note(note)
 
@@ -326,7 +348,7 @@ describe NotificationService, services: true do
     before do
       build_team(issue.project)
       add_users_with_subscription(issue.project, issue)
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
       update_custom_notification(:new_issue, @u_guest_custom, project)
       update_custom_notification(:new_issue, @u_custom_global)
     end
@@ -357,6 +379,7 @@ describe NotificationService, services: true do
       it "emails subscribers of the issue's labels" do
         subscriber = create(:user)
         label = create(:label, issues: [issue])
+        issue.reload
         label.toggle_subscription(subscriber)
         notification.new_issue(issue, @u_disabled)
 
@@ -377,6 +400,7 @@ describe NotificationService, services: true do
           project.team << [guest, :guest]
 
           label = create(:label, issues: [confidential_issue])
+          confidential_issue.reload
           label.toggle_subscription(non_member)
           label.toggle_subscription(author)
           label.toggle_subscription(assignee)
@@ -384,7 +408,7 @@ describe NotificationService, services: true do
           label.toggle_subscription(guest)
           label.toggle_subscription(admin)
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.new_issue(confidential_issue, @u_disabled)
 
@@ -399,6 +423,13 @@ describe NotificationService, services: true do
       end
     end
 
+    describe '#new_mentions_in_issue' do
+      let(:notification_method) { :new_mentions_in_issue }
+      let(:mentionable) { issue }
+
+      include_examples 'notifications for new mentions'
+    end
+
     describe '#reassigned_issue' do
       before do
         update_custom_notification(:reassign_issue, @u_guest_custom, project)
@@ -573,7 +604,7 @@ describe NotificationService, services: true do
           label_2.toggle_subscription(guest)
           label_2.toggle_subscription(admin)
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
 
@@ -700,7 +731,9 @@ describe NotificationService, services: true do
     before do
       build_team(merge_request.target_project)
       add_users_with_subscription(merge_request.target_project, merge_request)
-      ActionMailer::Base.deliveries.clear
+      update_custom_notification(:new_merge_request, @u_guest_custom, project)
+      update_custom_notification(:new_merge_request, @u_custom_global)
+      reset_delivered_emails!
     end
 
     describe '#new_merge_request' do
@@ -763,6 +796,13 @@ describe NotificationService, services: true do
       end
     end
 
+    describe '#new_mentions_in_merge_request' do
+      let(:notification_method) { :new_mentions_in_merge_request }
+      let(:mentionable) { merge_request }
+
+      include_examples 'notifications for new mentions'
+    end
+
     describe '#reassigned_merge_request' do
       before do
         update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
@@ -922,6 +962,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
+        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
+        notification.merge_mr(merge_request, @u_watcher)
+
+        should_not_email(@u_watcher)
+      end
+
       context 'participating' do
         context 'by assignee' do
           before do
@@ -1004,6 +1058,52 @@ describe NotificationService, services: true do
         end
       end
     end
+
+    describe "#resolve_all_discussions" do
+      it do
+        notification.resolve_all_discussions(merge_request, @u_disabled)
+
+        should_email(merge_request.assignee)
+        should_email(@u_watcher)
+        should_email(@u_participant_mentioned)
+        should_email(@subscriber)
+        should_email(@watcher_and_subscriber)
+        should_email(@u_guest_watcher)
+        should_not_email(@unsubscriber)
+        should_not_email(@u_participating)
+        should_not_email(@u_disabled)
+        should_not_email(@u_lazy_participant)
+      end
+
+      context 'participating' do
+        context 'by assignee' do
+          before do
+            merge_request.update_attribute(:assignee, @u_lazy_participant)
+            notification.resolve_all_discussions(merge_request, @u_disabled)
+          end
+
+          it { should_email(@u_lazy_participant) }
+        end
+
+        context 'by note' do
+          let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+          before { notification.resolve_all_discussions(merge_request, @u_disabled) }
+
+          it { should_email(@u_lazy_participant) }
+        end
+
+        context 'by author' do
+          before do
+            merge_request.author = @u_lazy_participant
+            merge_request.save
+            notification.resolve_all_discussions(merge_request, @u_disabled)
+          end
+
+          it { should_email(@u_lazy_participant) }
+        end
+      end
+    end
   end
 
   describe 'Projects' do
@@ -1011,7 +1111,7 @@ describe NotificationService, services: true do
 
     before do
       build_team(project)
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
     end
 
     describe '#project_was_moved' do
@@ -1029,6 +1129,101 @@ describe NotificationService, services: true do
     end
   end
 
+  describe 'GroupMember' do
+    describe '#decline_group_invite' do
+      let(:creator) { create(:user) }
+      let(:group) { create(:group) }
+      let(:member) { create(:user) }
+
+      before(:each) do
+        group.add_owner(creator)
+        group.add_developer(member, creator)
+      end
+
+      it do
+        group_member = group.members.first
+
+        expect do
+          notification.decline_group_invite(group_member)
+        end.to change { ActionMailer::Base.deliveries.size }.by(1)
+      end
+    end
+  end
+
+  describe 'ProjectMember' do
+    describe '#decline_group_invite' do
+      let(:project) { create(:project) }
+      let(:member) { create(:user) }
+
+      before(:each) do
+        project.team << [member, :developer, project.owner]
+      end
+
+      it do
+        project_member = project.members.first
+
+        expect do
+          notification.decline_project_invite(project_member)
+        end.to change { ActionMailer::Base.deliveries.size }.by(1)
+      end
+    end
+  end
+
+  context 'guest user in private project' do
+    let(:private_project) { create(:empty_project, :private) }
+    let(:guest) { create(:user) }
+    let(:developer) { create(:user) }
+    let(:assignee) { create(:user) }
+    let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
+    let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+    let(:note) { create(:note, noteable: merge_request, project: private_project) }
+
+    before do
+      private_project.team << [assignee, :developer]
+      private_project.team << [developer, :developer]
+      private_project.team << [guest, :guest]
+
+      ActionMailer::Base.deliveries.clear
+    end
+
+    it 'filters out guests when new note is created' do
+      expect(SentNotification).to receive(:record).with(merge_request, any_args).exactly(1).times
+
+      notification.new_note(note)
+
+      should_not_email(guest)
+      should_email(assignee)
+    end
+
+    it 'filters out guests when new merge request is created' do
+      notification.new_merge_request(merge_request1, @u_disabled)
+
+      should_not_email(guest)
+      should_email(assignee)
+    end
+
+    it 'filters out guests when merge request is closed' do
+      notification.close_mr(merge_request, developer)
+
+      should_not_email(guest)
+      should_email(assignee)
+    end
+
+    it 'filters out guests when merge request is reopened' do
+      notification.reopen_mr(merge_request, developer)
+
+      should_not_email(guest)
+      should_email(assignee)
+    end
+
+    it 'filters out guests when merge request is merged' do
+      notification.merge_mr(merge_request, developer)
+
+      should_not_email(guest)
+      should_email(assignee)
+    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 bbced59ff023b1c36fbdbbfbd7600e7af0bb1d0e..876bfaf085c77a3235460de6f298010a7e316321 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do
 
       context 'global builds_enabled false does not enable CI by default' do
         before do
-          @opts.merge!(builds_enabled: false)
+          project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
         end
 
         it { is_expected.to be_falsey }
@@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do
 
       context 'global builds_enabled true does enable CI by default' do
         before do
-          @opts.merge!(builds_enabled: true)
+          project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
         end
 
         it { is_expected.to be_truthy }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 29341c5e57e129e4c023a0d656237586caa56b72..7dcd03496bbcae7df6be43437d632d175e260041 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do
   let!(:project) { create(:project, namespace: user.namespace) }
   let!(:path) { project.repository.path_to_repo }
   let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
+  let!(:async) { false } # execute or async_execute
 
   context 'Sidekiq inline' do
     before do
@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do
     it { expect(Dir.exist?(remove_path)).to be_truthy }
   end
 
+  context 'async delete of project with private issue visibility' do
+    let!(:async) { true }
+
+    before do
+      project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+      # Run sidekiq immediately to check that renamed repository will be removed
+      Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+    end
+
+    it 'deletes the project' do
+      expect(Project.all).not_to include(project)
+      expect(Dir.exist?(path)).to be_falsey
+      expect(Dir.exist?(remove_path)).to be_falsey
+    end
+  end
+
   context 'container registry' do
     before do
       stub_container_registry_config(enabled: true)
@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do
   end
 
   def destroy_project(project, user, params)
-    Projects::DestroyService.new(project, user, params).execute
+    if async
+      Projects::DestroyService.new(project, user, params).async_execute
+    else
+      Projects::DestroyService.new(project, user, params).execute
+    end
   end
 end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ef2036c78b1ab8b387d2c61124fff330a8cb2851..64d15c0523c4fc72ac8e27f29191a4b9a3b5cb03 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
                              description: 'wow such project')
       @to_namespace = create(:namespace)
       @to_user = create(:user, namespace: @to_namespace)
+      @from_project.add_user(@to_user, :developer)
     end
 
     context 'fork project' do
+      context 'when forker is a guest' do
+        before do
+          @guest = create(:user)
+          @from_project.add_user(@guest, :guest)
+        end
+        subject { fork_project(@from_project, @guest) }
+
+        it { is_expected.not_to be_persisted }
+        it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
+      end
+
       describe "successfully creates project in the user namespace" do
         let(:to_project) { fork_project(@from_project, @to_user) }
 
+        it { expect(to_project).to be_persisted }
+        it { expect(to_project.errors).to be_empty }
         it { expect(to_project.owner).to eq(@to_user) }
         it { expect(to_project.namespace).to eq(@to_user.namespace) }
         it { expect(to_project.star_count).to be_zero }
@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
       it "fails due to validation, not transaction failure" do
         @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
         @to_project = fork_project(@from_project, @to_user)
-        expect(@existing_project.persisted?).to be_truthy
+        expect(@existing_project).to be_persisted
+
+        expect(@to_project).not_to be_persisted
         expect(@to_project.errors[:name]).to eq(['has already been taken'])
         expect(@to_project.errors[:path]).to eq(['has already been taken'])
       end
@@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
       @group = create(:group)
       @group.add_user(@group_owner, GroupMember::OWNER)
       @group.add_user(@developer,   GroupMember::DEVELOPER)
+      @project.add_user(@developer,   :developer)
+      @project.add_user(@group_owner, :developer)
       @opts = { namespace: @group }
     end
 
     context 'fork project for group' do
       it 'group owner successfully forks project into the group' do
         to_project = fork_project(@project, @group_owner, @opts)
+
+        expect(to_project).to             be_persisted
+        expect(to_project.errors).to      be_empty
         expect(to_project.owner).to       eq(@group)
         expect(to_project.namespace).to   eq(@group)
         expect(to_project.name).to        eq(@project.name)
         expect(to_project.path).to        eq(@project.path)
         expect(to_project.description).to eq(@project.description)
-        expect(to_project.star_count).to     be_zero
+        expect(to_project.star_count).to  be_zero
       end
     end
 
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index ad0d58672b3df13fd90e703932cfbe30eeb469d9..57a5aa5cedc11d22082c932466f73bce98f74fcf 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -4,15 +4,20 @@ describe Projects::HousekeepingService do
   subject { Projects::HousekeepingService.new(project) }
   let(:project) { create :project }
 
-  describe 'execute' do
-    before do
-      project.pushes_since_gc = 3
-      project.save!
-    end
+  before do
+    project.reset_pushes_since_gc
+  end
+
+  after do
+    project.reset_pushes_since_gc
+  end
 
+  describe '#execute' do
     it 'enqueues a sidekiq job' do
-      expect(subject).to receive(:try_obtain_lease).and_return(true)
-      expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id)
+      expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
+      expect(subject).to receive(:lease_key).and_return(:the_lease_key)
+      expect(subject).to receive(:task).and_return(:the_task)
+      expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid)
 
       subject.execute
       expect(project.reload.pushes_since_gc).to eq(0)
@@ -32,12 +37,12 @@ describe Projects::HousekeepingService do
       it 'does not reset pushes_since_gc' do
         expect do
           expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
-        end.not_to change { project.pushes_since_gc }.from(3)
+        end.not_to change { project.pushes_since_gc }
       end
     end
   end
 
-  describe 'needed?' do
+  describe '#needed?' do
     it 'when the count is low enough' do
       expect(subject.needed?).to eq(false)
     end
@@ -48,25 +53,33 @@ describe Projects::HousekeepingService do
     end
   end
 
-  describe 'increment!' do
-    let(:lease_key) { "project_housekeeping:increment!:#{project.id}" }
-
+  describe '#increment!' do
     it 'increments the pushes_since_gc counter' do
-      lease = double(:lease, try_obtain: true)
-      expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
-
       expect do
         subject.increment!
       end.to change { project.pushes_since_gc }.from(0).to(1)
     end
+  end
 
-    it 'does not increment when no lease can be obtained' do
-      lease = double(:lease, try_obtain: false)
-      expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
+  it 'uses all three kinds of housekeeping we offer' do
+    allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
+    allow(subject).to receive(:lease_key).and_return(:the_lease_key)
 
-      expect do
-        subject.increment!
-      end.not_to change { project.pushes_since_gc }
+    # At push 200
+    expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid).
+      exactly(1).times
+    # At push 50, 100, 150
+    expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid).
+      exactly(3).times
+    # At push 10, 20, ... (except those above)
+    expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid).
+      exactly(16).times
+
+    201.times do
+      subject.increment!
+      subject.execute if subject.needed?
     end
+
+    expect(project.pushes_since_gc).to eq(1)
   end
 end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index d5d4d7c56ef5d492f7a03a861b4797d0113c9cdd..ab6e8f537bac91e1f6d89c55adf0c848eacae554 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -108,6 +108,16 @@ describe Projects::ImportService, services: true do
         expect(result[:status]).to eq :error
         expect(result[:message]).to eq 'Github: failed to connect API'
       end
+
+      it 'expires existence cache after error' do
+        allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true)
+
+        expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+        expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
+        expect_any_instance_of(Repository).to receive(:expire_exists_cache).and_call_original
+
+        subject.execute
+      end
     end
 
     def stub_github_omniauth_provider
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 57c71544dfffada67f92d9a80c6f47e582bc6510..1540b90163a2f98e302442c6a46af6b8a934feef 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do
       it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
     end
   end
+
+  context 'missing group labels applied to issues or merge requests' do
+    it 'delegates tranfer to Labels::TransferService' do
+      group.add_owner(user)
+
+      expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original
+
+      transfer_project(project, user, group)
+    end
+  end
 end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7d4eff3b6ef6c14c3f9c5cef4c749cdfa4df3d7c
--- /dev/null
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProtectedBranches::CreateService, services: true do
+  let(:project) { create(:empty_project) }
+  let(:user) { project.owner }
+  let(:params) do
+    {
+      name: 'master',
+      merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
+      push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+    }
+  end
+
+  describe '#execute' do
+    subject(:service) { described_class.new(project, user, params) }
+
+    it 'creates a new protected branch' do
+      expect { service.execute }.to change(ProtectedBranch, :count).by(1)
+      expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+      expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+    end
+  end
+end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b57e338b78298dd3c34ce0fb75429083c4830064
--- /dev/null
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -0,0 +1,505 @@
+require 'spec_helper'
+
+describe SlashCommands::InterpretService, services: true do
+  let(:project) { create(:empty_project, :public) }
+  let(:developer) { create(:user) }
+  let(:issue) { create(:issue, project: project) }
+  let(:milestone) { create(:milestone, project: project, title: '9.10') }
+  let(:inprogress) { create(:label, project: project, title: 'In Progress') }
+  let(:bug) { create(:label, project: project, title: 'Bug') }
+
+  before do
+    project.team << [developer, :developer]
+  end
+
+  describe '#execute' do
+    let(:service) { described_class.new(project, developer) }
+    let(:merge_request) { create(:merge_request, source_project: project) }
+
+    shared_examples 'reopen command' do
+      it 'returns state_event: "reopen" if content contains /reopen' do
+        issuable.close!
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(state_event: 'reopen')
+      end
+    end
+
+    shared_examples 'close command' do
+      it 'returns state_event: "close" if content contains /close' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(state_event: 'close')
+      end
+    end
+
+    shared_examples 'title command' do
+      it 'populates title: "A brand new title" if content contains /title A brand new title' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(title: 'A brand new title')
+      end
+    end
+
+    shared_examples 'assign command' do
+      it 'fetches assignee and populates assignee_id if content contains /assign' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(assignee_id: developer.id)
+      end
+    end
+
+    shared_examples 'unassign command' do
+      it 'populates assignee_id: nil if content contains /unassign' do
+        issuable.update(assignee_id: developer.id)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(assignee_id: nil)
+      end
+    end
+
+    shared_examples 'milestone command' do
+      it 'fetches milestone and populates milestone_id if content contains /milestone' do
+        milestone # populate the milestone
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(milestone_id: milestone.id)
+      end
+    end
+
+    shared_examples 'remove_milestone command' do
+      it 'populates milestone_id: nil if content contains /remove_milestone' do
+        issuable.update(milestone_id: milestone.id)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(milestone_id: nil)
+      end
+    end
+
+    shared_examples 'label command' do
+      it 'fetches label ids and populates add_label_ids if content contains /label' do
+        bug # populate the label
+        inprogress # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(add_label_ids: [bug.id, inprogress.id])
+      end
+    end
+
+    shared_examples 'multiple label command' do
+      it 'fetches label ids and populates add_label_ids if content contains multiple /label' do
+        bug # populate the label
+        inprogress # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(add_label_ids: [inprogress.id, bug.id])
+      end
+    end
+
+    shared_examples 'multiple label with same argument' do
+      it 'prevents duplicate label ids and populates add_label_ids if content contains multiple /label' do
+        inprogress # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(add_label_ids: [inprogress.id])
+      end
+    end
+
+    shared_examples 'unlabel command' do
+      it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
+        issuable.update(label_ids: [inprogress.id]) # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(remove_label_ids: [inprogress.id])
+      end
+    end
+
+    shared_examples 'multiple unlabel command' do
+      it 'fetches label ids and populates remove_label_ids if content contains  mutiple /unlabel' do
+        issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
+      end
+    end
+
+    shared_examples 'unlabel command with no argument' do
+      it 'populates label_ids: [] if content contains /unlabel with no arguments' do
+        issuable.update(label_ids: [inprogress.id]) # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(label_ids: [])
+      end
+    end
+
+    shared_examples 'relabel command' do
+      it 'populates label_ids: [] if content contains /relabel' do
+        issuable.update(label_ids: [bug.id]) # populate the label
+        inprogress # populate the label
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(label_ids: [inprogress.id])
+      end
+    end
+
+    shared_examples 'todo command' do
+      it 'populates todo_event: "add" if content contains /todo' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(todo_event: 'add')
+      end
+    end
+
+    shared_examples 'done command' do
+      it 'populates todo_event: "done" if content contains /done' do
+        TodoService.new.mark_todo(issuable, developer)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(todo_event: 'done')
+      end
+    end
+
+    shared_examples 'subscribe command' do
+      it 'populates subscription_event: "subscribe" if content contains /subscribe' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(subscription_event: 'subscribe')
+      end
+    end
+
+    shared_examples 'unsubscribe command' do
+      it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
+        issuable.subscribe(developer)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(subscription_event: 'unsubscribe')
+      end
+    end
+
+    shared_examples 'due command' do
+      it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(due_date: defined?(expected_date) ? expected_date : Date.new(2016, 8, 28))
+      end
+    end
+
+    shared_examples 'remove_due_date command' do
+      it 'populates due_date: nil if content contains /remove_due_date' do
+        issuable.update(due_date: Date.today)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(due_date: nil)
+      end
+    end
+
+    shared_examples 'wip command' do
+      it 'returns wip_event: "wip" if content contains /wip' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(wip_event: 'wip')
+      end
+    end
+
+    shared_examples 'unwip command' do
+      it 'returns wip_event: "unwip" if content contains /wip' do
+        issuable.update(title: issuable.wip_title)
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to eq(wip_event: 'unwip')
+      end
+    end
+
+    shared_examples 'empty command' do
+      it 'populates {} if content contains an unsupported command' do
+        _, updates = service.execute(content, issuable)
+
+        expect(updates).to be_empty
+      end
+    end
+
+    it_behaves_like 'reopen command' do
+      let(:content) { '/reopen' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'reopen command' do
+      let(:content) { '/reopen' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'close command' do
+      let(:content) { '/close' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'close command' do
+      let(:content) { '/close' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'title command' do
+      let(:content) { '/title A brand new title' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'title command' do
+      let(:content) { '/title A brand new title' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/title' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'assign command' do
+      let(:content) { "/assign @#{developer.username}" }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'assign command' do
+      let(:content) { "/assign @#{developer.username}" }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/assign @abcd1234' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/assign' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unassign command' do
+      let(:content) { '/unassign' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unassign command' do
+      let(:content) { '/unassign' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'milestone command' do
+      let(:content) { "/milestone %#{milestone.title}" }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'milestone command' do
+      let(:content) { "/milestone %#{milestone.title}" }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'remove_milestone command' do
+      let(:content) { '/remove_milestone' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'remove_milestone command' do
+      let(:content) { '/remove_milestone' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'label command' do
+      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'label command' do
+      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'multiple label command' do
+      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'multiple label with same argument' do
+      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
+      let(:issuable) { issue }
+    end	
+
+    it_behaves_like 'unlabel command' do
+      let(:content) { %(/unlabel ~"#{inprogress.title}") }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unlabel command' do
+      let(:content) { %(/unlabel ~"#{inprogress.title}") }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'multiple unlabel command' do
+      let(:content) { %(/unlabel ~"#{inprogress.title}" \n/unlabel ~#{bug.title}) }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unlabel command with no argument' do
+      let(:content) { %(/unlabel) }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unlabel command with no argument' do
+      let(:content) { %(/unlabel) }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'relabel command' do
+      let(:content) { %(/relabel ~"#{inprogress.title}") }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'relabel command' do
+      let(:content) { %(/relabel ~"#{inprogress.title}") }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'todo command' do
+      let(:content) { '/todo' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'todo command' do
+      let(:content) { '/todo' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'done command' do
+      let(:content) { '/done' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'done command' do
+      let(:content) { '/done' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'subscribe command' do
+      let(:content) { '/subscribe' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'subscribe command' do
+      let(:content) { '/subscribe' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'unsubscribe command' do
+      let(:content) { '/unsubscribe' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'unsubscribe command' do
+      let(:content) { '/unsubscribe' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'due command' do
+      let(:content) { '/due 2016-08-28' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'due command' do
+      let(:content) { '/due tomorrow' }
+      let(:issuable) { issue }
+      let(:expected_date) { Date.tomorrow }
+    end
+
+    it_behaves_like 'due command' do
+      let(:content) { '/due 5 days from now' }
+      let(:issuable) { issue }
+      let(:expected_date) { 5.days.from_now.to_date }
+    end
+
+    it_behaves_like 'due command' do
+      let(:content) { '/due in 2 days' }
+      let(:issuable) { issue }
+      let(:expected_date) { 2.days.from_now.to_date }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/due foo bar' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/due 2016-08-28' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'remove_due_date command' do
+      let(:content) { '/remove_due_date' }
+      let(:issuable) { issue }
+    end
+
+    it_behaves_like 'wip command' do
+      let(:content) { '/wip' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'unwip command' do
+      let(:content) { '/wip' }
+      let(:issuable) { merge_request }
+    end
+
+    it_behaves_like 'empty command' do
+      let(:content) { '/remove_due_date' }
+      let(:issuable) { merge_request }
+    end
+
+    context 'when current_user cannot :admin_issue' do
+      let(:visitor) { create(:user) }
+      let(:issue) { create(:issue, project: project, author: visitor) }
+      let(:service) { described_class.new(project, visitor) }
+
+      it_behaves_like 'empty command' do
+        let(:content) { "/assign @#{developer.username}" }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { '/unassign' }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { "/milestone %#{milestone.title}" }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { '/remove_milestone' }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { %(/unlabel ~"#{inprogress.title}") }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { %(/relabel ~"#{inprogress.title}") }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { '/due tomorrow' }
+        let(:issuable) { issue }
+      end
+
+      it_behaves_like 'empty command' do
+        let(:content) { '/remove_due_date' }
+        let(:issuable) { issue }
+      end
+    end
+  end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 00427d6db2a6c29dd1d7313df647940daf0e555c..5bb107fdd859065da5dab9873583f1d8d5974df5 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -40,6 +40,12 @@ describe SystemNoteService, services: true do
     describe 'note body' do
       let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
 
+      describe 'comparison diff link line' do
+        it 'adds the comparison text' do
+          expect(note_lines[2]).to match "[Compare with previous version]"
+        end
+      end
+
       context 'without existing commits' do
         it 'adds a message header' do
           expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
@@ -48,7 +54,7 @@ describe SystemNoteService, services: true do
         it 'adds a message line for each commit' do
           new_commits.each_with_index do |commit, i|
             # Skip the header
-            expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}"
+            expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}"
           end
         end
       end
@@ -75,7 +81,7 @@ describe SystemNoteService, services: true do
             end
 
             it 'includes a commit count' do
-              expect(summary_line).to end_with " - 2 commits from branch `feature`"
+              expect(summary_line).to end_with " - 26 commits from branch `feature`"
             end
           end
 
@@ -85,7 +91,7 @@ describe SystemNoteService, services: true do
             end
 
             it 'includes a commit count' do
-              expect(summary_line).to end_with " - 2 commits from branch `feature`"
+              expect(summary_line).to end_with " - 26 commits from branch `feature`"
             end
           end
 
@@ -330,13 +336,13 @@ describe SystemNoteService, services: true do
             let(:mentioner) { project2.repository.commit }
 
             it 'references the mentioning commit' do
-              expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
+              expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}"
             end
           end
 
           context 'from non-Commit' do
             it 'references the mentioning object' do
-              expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
+              expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}"
             end
           end
         end
@@ -346,13 +352,13 @@ describe SystemNoteService, services: true do
             let(:mentioner) { project.repository.commit }
 
             it 'references the mentioning commit' do
-              expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
+              expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}"
             end
           end
 
           context 'from non-Commit' do
             it 'references the mentioning object' do
-              expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
+              expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}"
             end
           end
         end
@@ -362,7 +368,7 @@ describe SystemNoteService, services: true do
 
   describe '.cross_reference?' do
     it 'is truthy when text begins with expected text' do
-      expect(described_class.cross_reference?('mentioned in something')).to be_truthy
+      expect(described_class.cross_reference?('Mentioned in something')).to be_truthy
     end
 
     it 'is falsey when text does not begin with expected text' do
@@ -445,7 +451,7 @@ describe SystemNoteService, services: true do
     end
 
     context 'commit with cross-reference from fork' do
-      let(:author2) { create(:user) }
+      let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
       let(:forked_project) { Projects::ForkService.new(project, author2).execute }
       let(:commit2) { forked_project.commit }
 
@@ -525,61 +531,47 @@ describe SystemNoteService, services: true do
   include JiraServiceHelper
 
   describe 'JIRA integration' do
-    let(:project)    { create(:project) }
-    let(:author)     { create(:user) }
-    let(:issue)      { create(:issue, project: project) }
-    let(:mergereq)   { create(:merge_request, :simple, target_project: project, source_project: project) }
-    let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
-    let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
-    let(:commit)     { project.commit }
+    let(:project)         { create(:jira_project) }
+    let(:author)          { create(:user) }
+    let(:issue)           { create(:issue, project: project) }
+    let(:mergereq)        { create(:merge_request, :simple, target_project: project, source_project: project) }
+    let(:jira_issue)      { ExternalIssue.new("JIRA-1", project)}
+    let(:jira_tracker)    { project.jira_service }
+    let(:commit)          { project.commit }
+    let(:comment_url)     { jira_api_comment_url(jira_issue.id) }
+    let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." }
+
+    before { stub_jira_urls(jira_issue.id) }
 
     context 'in JIRA issue tracker' do
-      before do
-        jira_service_settings
-        WebMock.stub_request(:post, jira_api_comment_url)
-      end
-
-      after do
-        jira_tracker.destroy!
-      end
+      before { jira_service_settings }
 
       describe "new reference" do
-        before do
-          WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
-        end
-
         subject { described_class.cross_reference(jira_issue, commit, author) }
 
-        it { is_expected.to eq(jira_status_message) }
-      end
-
-      describe "existing reference" do
-        before do
-          message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
-          WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
-        end
-
-        subject { described_class.cross_reference(jira_issue, commit, author) }
-        it { is_expected.not_to eq(jira_status_message) }
+        it { is_expected.to eq(success_message) }
       end
     end
 
     context 'issue from an issue' do
       context 'in JIRA issue tracker' do
-        before do
-          jira_service_settings
-          WebMock.stub_request(:post, jira_api_comment_url)
-          WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
-        end
-
-        after do
-          jira_tracker.destroy!
-        end
+        before { jira_service_settings }
 
         subject { described_class.cross_reference(jira_issue, issue, author) }
 
-        it { is_expected.to eq(jira_status_message) }
+        it { is_expected.to eq(success_message) }
       end
     end
+
+    describe "existing reference" do
+      before do
+        message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'"
+        allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
+      end
+
+      subject { described_class.cross_reference(jira_issue, commit, author) }
+
+      it { is_expected.not_to eq(success_message) }
+    end
   end
 end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 6c3cbeae13c8d6948fe4b328e8fe8858a0288fd7..ed55791d24e46e71564238c8af75ead2e57726e7 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -145,6 +145,14 @@ describe TodoService, services: true do
       end
     end
 
+    describe '#destroy_issue' do
+      it 'refresh the todos count cache for the user' do
+        expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+        service.destroy_issue(issue, john_doe)
+      end
+    end
+
     describe '#reassigned_issue' do
       it 'creates a pending todo for new assignee' do
         unassigned_issue.update_attribute(:assignee, john_doe)
@@ -194,12 +202,12 @@ describe TodoService, services: true do
       end
     end
 
-    describe '#mark_todos_as_done' do
-      it 'marks related todos for the user as done' do
-        first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-        second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+    shared_examples 'marking todos as done' do |meth|
+      let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+      let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
 
-        service.mark_todos_as_done([first_todo, second_todo], john_doe)
+      it 'marks related todos for the user as done' do
+        service.send(meth, collection, john_doe)
 
         expect(first_todo.reload).to be_done
         expect(second_todo.reload).to be_done
@@ -207,20 +215,30 @@ describe TodoService, services: true do
 
       describe 'cached counts' do
         it 'updates when todos change' do
-          todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-
           expect(john_doe.todos_done_count).to eq(0)
-          expect(john_doe.todos_pending_count).to eq(1)
+          expect(john_doe.todos_pending_count).to eq(2)
           expect(john_doe).to receive(:update_todos_count_cache).and_call_original
 
-          service.mark_todos_as_done([todo], john_doe)
+          service.send(meth, collection, john_doe)
 
-          expect(john_doe.todos_done_count).to eq(1)
+          expect(john_doe.todos_done_count).to eq(2)
           expect(john_doe.todos_pending_count).to eq(0)
         end
       end
     end
 
+    describe '#mark_todos_as_done' do
+      it_behaves_like 'marking todos as done', :mark_todos_as_done do
+        let(:collection) { [first_todo, second_todo] }
+      end
+    end
+
+    describe '#mark_todos_as_done_by_ids' do
+      it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+        let(:collection) { [first_todo, second_todo].map(&:id) }
+      end
+    end
+
     describe '#new_note' do
       let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
       let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
@@ -290,6 +308,18 @@ describe TodoService, services: true do
         should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
       end
     end
+
+    describe '#todo_exists?' do
+      it 'returns false when no todo exist for the given issuable' do
+        expect(service.todo_exist?(unassigned_issue, author)).to be_falsy
+      end
+
+      it 'returns true when a todo exist for the given issuable' do
+        service.mark_todo(unassigned_issue, author)
+
+        expect(service.todo_exist?(unassigned_issue, author)).to be_truthy
+      end
+    end
   end
 
   describe 'Merge Requests' do
@@ -315,7 +345,7 @@ describe TodoService, services: true do
         service.new_merge_request(mr_assigned, author)
 
         should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
-        should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+        should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
         should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
         should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
         should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -327,7 +357,7 @@ describe TodoService, services: true do
         service.update_merge_request(mr_assigned, author)
 
         should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
-        should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+        should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
         should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
         should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
         should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -351,6 +381,7 @@ describe TodoService, services: true do
           should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
           should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
           should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+          should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
         end
 
         it 'does not raise an error when description not change' do
@@ -372,6 +403,14 @@ describe TodoService, services: true do
       end
     end
 
+    describe '#destroy_merge_request' do
+      it 'refresh the todos count cache for the user' do
+        expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+        service.destroy_merge_request(mr_assigned, john_doe)
+      end
+    end
+
     describe '#reassigned_merge_request' do
       it 'creates a pending todo for new assignee' do
         mr_unassigned.update_attribute(:assignee, john_doe)
@@ -392,6 +431,11 @@ describe TodoService, services: true do
 
         should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
       end
+
+      it 'does not create a todo for guests' do
+        service.reassigned_merge_request(mr_assigned, author)
+        should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+      end
     end
 
     describe '#merge_merge_request' do
@@ -403,6 +447,11 @@ describe TodoService, services: true do
         expect(first_todo.reload).to be_done
         expect(second_todo.reload).to be_done
       end
+
+      it 'does not create todo for guests' do
+        service.merge_merge_request(mr_assigned, john_doe)
+        should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+      end
     end
 
     describe '#new_award_emoji' do
@@ -457,6 +506,13 @@ describe TodoService, services: true do
 
         should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request)
       end
+
+      it 'does not create todo for guests' do
+        note_on_merge_request = create :note_on_merge_request, project: project, noteable: mr_assigned, note: mentions
+        service.new_note(note_on_merge_request, author)
+
+        should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
+      end
     end
   end
 
@@ -474,6 +530,7 @@ describe TodoService, services: true do
 
   describe '#mark_todos_as_done' do
     let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+    let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
 
     it 'marks a relation of todos as done' do
       create(:todo, :mentioned, user: john_doe, target: issue, project: project)
@@ -496,6 +553,26 @@ describe TodoService, services: true do
       expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
     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
+
+      it 'returns the number 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)
+      end
+
+      it 'returns 0 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)
+      end
+    end
+
     it 'caches the number of todos of a user', :caching do
       create(:todo, :mentioned, user: john_doe, target: issue, project: project)
       todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index 6f8f7109e143245241dd88b69a402c97918ff3e5..b507d38f472c719e50595ca1db74a464312bb2ae 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -1,4 +1,5 @@
 require 'simplecov'
+require 'active_support/core_ext/numeric/time'
 
 module SimpleCovEnv
   extend self
@@ -48,7 +49,7 @@ module SimpleCovEnv
       add_group 'Uploaders', 'app/uploaders'
       add_group 'Validators', 'app/validators'
 
-      merge_timeout 7200
+      merge_timeout 365.days
     end
   end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2e2aa7c4fc04e7681ce6d37d50a0055b23e4064a..73cf4c9a24cb123e9487f6ce6c979a6261b2c0b0 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -9,7 +9,7 @@ require 'shoulda/matchers'
 require 'sidekiq/testing/inline'
 require 'rspec/retry'
 
-if ENV['CI']
+if ENV['CI'] && !ENV['NO_KNAPSACK']
   require 'knapsack'
   Knapsack::Adapters::RSpecAdapter.bind
 end
@@ -26,13 +26,15 @@ RSpec.configure do |config|
   config.verbose_retry = true
   config.display_try_failure_messages = true
 
-  config.include Devise::TestHelpers, type: :controller
-  config.include LoginHelpers,        type: :feature
-  config.include LoginHelpers,        type: :request
+  config.include Devise::Test::ControllerHelpers,   type: :controller
+  config.include Warden::Test::Helpers, type: :request
+  config.include LoginHelpers,          type: :feature
+  config.include SearchHelpers,         type: :feature
   config.include StubConfiguration
   config.include EmailHelpers
   config.include TestEnv
   config.include ActiveJob::TestHelper
+  config.include ActiveSupport::Testing::TimeHelpers
   config.include StubGitlabCalls
   config.include StubGitlabData
 
@@ -49,6 +51,12 @@ RSpec.configure do |config|
     example.run
     Rails.cache = caching_store
   end
+
+  config.around(:each, :redis) do |example|
+    Gitlab::Redis.with(&:flushall)
+    example.run
+    Gitlab::Redis.with(&:flushall)
+  end
 end
 
 FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e42d727672be009d12e71c0428e28aca0bd91ad7
--- /dev/null
+++ b/spec/support/api/schema_matcher.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :match_response_schema do |schema, **options|
+  match do |response|
+    schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas"
+    schema_path = "#{schema_directory}/#{schema}.json"
+
+    JSON::Validator.validate!(schema_path, response.body, options)
+  end
+end
diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eb5da662ab5f9c26aee739fcf971337ee818476b
--- /dev/null
+++ b/spec/support/banzai/reference_filter_shared_examples.rb
@@ -0,0 +1,13 @@
+# Specs for reference links containing HTML.
+#
+# Requires a reference:
+#   let(:reference) { '#42' }
+shared_examples 'a reference containing an element node' do
+  let(:inner_html) { 'element <code>node</code> inside' }
+  let(:reference_with_element) { %{<a href="#{reference}">#{inner_html}</a>} }
+
+  it 'does not escape inner html' do
+    doc = reference_filter(reference_with_element)
+    expect(doc.children.first.inner_html).to eq(inner_html)
+  end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75c95d70951bed25b429df63fed06a6005712b60
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -0,0 +1,69 @@
+module CycleAnalyticsHelpers
+  def create_commit_referencing_issue(issue, branch_name: random_git_name)
+    project.repository.add_branch(user, branch_name, 'master')
+    create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+  end
+
+  def create_commit(message, project, user, branch_name, count: 1)
+    oldrev = project.repository.commit(branch_name).sha
+    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)
+      project.repository.commit(commit_sha)
+
+      commit_sha
+    end
+
+    GitPushService.new(project,
+                       user,
+                       oldrev: oldrev,
+                       newrev: commit_shas.last,
+                       ref: 'refs/heads/master').execute
+  end
+
+  def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
+    if !source_branch || project.repository.commit(source_branch).blank?
+      source_branch = random_git_name
+      project.repository.add_branch(user, source_branch, 'master')
+    end
+
+    sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+    project.repository.commit(sha)
+
+    opts = {
+      title: 'Awesome merge_request',
+      description: message || "Fixes #{issue.to_reference}",
+      source_branch: source_branch,
+      target_branch: 'master'
+    }
+
+    MergeRequests::CreateService.new(project, user, opts).execute
+  end
+
+  def merge_merge_requests_closing_issue(issue)
+    merge_requests = issue.closed_by_merge_requests(user)
+
+    merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+  end
+
+  def deploy_master(environment: 'production')
+    CreateDeploymentService.new(project, user, {
+                                  environment: environment,
+                                  ref: 'master',
+                                  tag: false,
+                                  sha: project.repository.commit('master').sha
+                                }).execute
+  end
+end
+
+RSpec.configure do |config|
+  config.include CycleAnalyticsHelpers
+end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e19a6c92e2e29c7d5b6b1adbe39760da44ddc2e
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -0,0 +1,161 @@
+# rubocop:disable Metrics/AbcSize
+
+# Note: The ABC size is large here because we have a method generating test cases with
+#       multiple nested contexts. This shouldn't count as a violation.
+
+module CycleAnalyticsHelpers
+  module TestGeneration
+    # Generate the most common set of specs that all cycle analytics phases need to have.
+    #
+    # Arguments:
+    #
+    #                  phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
+    #                data_fn: A function that returns a hash, constituting initial data for the test case
+    #  start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+    #                         `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+    #                         Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase.
+    #    end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+    #                         `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+    #                         Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase.
+    #          before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
+    #                post_fn: Code that needs to be run after running the end time conditions.
+
+    def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
+      combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
+      combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
+
+      scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
+      scenarios.each do |start_time_conditions, end_time_conditions|
+        context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+          context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+            it "finds the median of available durations between the two conditions" do
+              time_differences = Array.new(5) do |index|
+                data = data_fn[self]
+                start_time = (index * 10).days.from_now
+                end_time = start_time + rand(1..5).days
+
+                start_time_conditions.each do |condition_name, condition_fn|
+                  Timecop.freeze(start_time) { condition_fn[self, data] }
+                end
+
+                # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+                Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+                end_time_conditions.each do |condition_name, condition_fn|
+                  Timecop.freeze(end_time) { condition_fn[self, data] }
+                end
+
+                Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+                end_time - start_time
+              end
+
+              median_time_difference = time_differences.sort[2]
+              expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+            end
+
+            context "when the data belongs to another project" do
+              let(:other_project) { create(:project) }
+
+              it "returns nil" do
+                # Use a stub to "trick" the data/condition functions
+                # into using another project. This saves us from having to
+                # define separate data/condition functions for this particular
+                # test case.
+                allow(self).to receive(:project) { other_project }
+
+                5.times do
+                  data = data_fn[self]
+                  start_time = Time.now
+                  end_time = rand(1..10).days.from_now
+
+                  start_time_conditions.each do |condition_name, condition_fn|
+                    Timecop.freeze(start_time) { condition_fn[self, data] }
+                  end
+
+                  end_time_conditions.each do |condition_name, condition_fn|
+                    Timecop.freeze(end_time) { condition_fn[self, data] }
+                  end
+
+                  Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+                end
+
+                # Turn off the stub before checking assertions
+                allow(self).to receive(:project).and_call_original
+
+                expect(subject.send(phase)).to be_nil
+              end
+            end
+
+            context "when the end condition happens before the start condition" do
+              it 'returns nil' do
+                data = data_fn[self]
+                start_time = Time.now
+                end_time = start_time + rand(1..5).days
+
+                # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+                Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+                end_time_conditions.each do |condition_name, condition_fn|
+                  Timecop.freeze(start_time) { condition_fn[self, data] }
+                end
+
+                start_time_conditions.each do |condition_name, condition_fn|
+                  Timecop.freeze(end_time) { condition_fn[self, data] }
+                end
+
+                Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+                expect(subject.send(phase)).to be_nil
+              end
+            end
+          end
+        end
+
+        context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
+          context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+            it "returns nil" do
+              5.times do
+                data = data_fn[self]
+                end_time = rand(1..10).days.from_now
+
+                end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+                  Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
+                end
+
+                Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+              end
+
+              expect(subject.send(phase)).to be_nil
+            end
+          end
+        end
+
+        context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+          context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
+            it "returns nil" do
+              5.times do
+                data = data_fn[self]
+                start_time = Time.now
+
+                start_time_conditions.each do |condition_name, condition_fn|
+                  Timecop.freeze(start_time) { condition_fn[self, data] }
+                end
+
+                post_fn[self, data] if post_fn
+              end
+
+              expect(subject.send(phase)).to be_nil
+            end
+          end
+        end
+      end
+
+      context "when none of the start / end conditions are matched" do
+        it "returns nil" do
+          expect(subject.send(phase)).to be_nil
+        end
+      end
+    end
+  end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index e0dbc9aa84c786f3ce2f3fb12b6d04d9eb72e66e..ac38e31b77e0e707c2fb4895776e1e4536394cb9 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -15,7 +15,7 @@ RSpec.configure do |config|
     DatabaseCleaner.start
   end
 
-  config.after(:each) do
+  config.append_after(:each) do
     DatabaseCleaner.clean
   end
 end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index a85ab22ce36fc814a2f5603d8d5f528a484f46ca..3e979f2f470c99a2f7cc4ff90e2d40a5f585425e 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,13 +1,33 @@
 module EmailHelpers
-  def sent_to_user?(user)
-    ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+  def sent_to_user?(user, recipients = email_recipients)
+    recipients.include?(user.notification_email)
   end
 
-  def should_email(user)
-    expect(sent_to_user?(user)).to be_truthy
+  def reset_delivered_emails!
+    ActionMailer::Base.deliveries.clear
   end
 
-  def should_not_email(user)
-    expect(sent_to_user?(user)).to be_falsey
+  def should_only_email(*users, kind: :to)
+    recipients = email_recipients(kind: kind)
+
+    users.each { |user| should_email(user, recipients) }
+
+    expect(recipients.count).to eq(users.count)
+  end
+
+  def should_email(user, recipients = email_recipients)
+    expect(sent_to_user?(user, recipients)).to be_truthy
+  end
+
+  def should_not_email(user, recipients = email_recipients)
+    expect(sent_to_user?(user, recipients)).to be_falsey
+  end
+
+  def should_not_email_anyone
+    expect(ActionMailer::Base.deliveries).to be_empty
+  end
+
+  def email_recipients(kind: :to)
+    ActionMailer::Base.deliveries.flat_map(&kind)
   end
 end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index f550e9a0160f27c7568084146c168ec7276e097c..8c407b867fef5683ed5df7945a3880772307b3c2 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -1,6 +1,9 @@
 class FakeU2fDevice
-  def initialize(page)
+  attr_reader :name
+
+  def initialize(page, name)
     @page = page
+    @name = name
   end
   
   def respond_to_u2f_registration
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e3b8f2b23e9c477623b3593d55261a69c128d08
--- /dev/null
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -0,0 +1,261 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It takes a `issuable_type`, and expect an `issuable`.
+
+shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
+  include SlashCommandsHelpers
+  include WaitForAjax
+
+  let(:master) { create(:user) }
+  let(:assignee) { create(:user, username: 'bob') }
+  let(:guest) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+  let!(:label_bug) { create(:label, project: project, title: 'bug') }
+  let!(:label_feature) { create(:label, project: project, title: 'feature') }
+  let(:new_url_opts) { {} }
+
+  before do
+    project.team << [master, :master]
+    project.team << [assignee, :developer]
+    project.team << [guest, :guest]
+    login_with(master)
+  end
+
+  after do
+    # Ensure all outstanding Ajax requests are complete to avoid database deadlocks
+    wait_for_ajax
+  end
+
+  describe "new #{issuable_type}" do
+    context 'with commands in the description' do
+      it "creates the #{issuable_type} and interpret commands accordingly" do
+        visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts)
+        fill_in "#{issuable_type}_title", with: 'bug 345'
+        fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\""
+        click_button "Submit #{issuable_type}".humanize
+
+        issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+        expect(issuable.description).to eq "bug description"
+        expect(issuable.labels).to eq [label_bug]
+        expect(issuable.milestone).to eq milestone
+        expect(page).to have_content 'bug 345'
+        expect(page).to have_content 'bug description'
+      end
+    end
+  end
+
+  describe "note on #{issuable_type}" do
+    before do
+      visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+    end
+
+    context 'with a note containing commands' do
+      it 'creates a note without the commands and interpret the commands accordingly' do
+        write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+
+        expect(page).to have_content 'Awesome!'
+        expect(page).not_to have_content '/assign @bob'
+        expect(page).not_to have_content '/label ~bug'
+        expect(page).not_to have_content '/milestone %"ASAP"'
+
+        issuable.reload
+        note = issuable.notes.user.first
+
+        expect(note.note).to eq "Awesome!"
+        expect(issuable.assignee).to eq assignee
+        expect(issuable.labels).to eq [label_bug]
+        expect(issuable.milestone).to eq milestone
+      end
+    end
+
+    context 'with a note containing only commands' do
+      it 'does not create a note but interpret the commands accordingly' do
+        write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+
+        expect(page).not_to have_content '/assign @bob'
+        expect(page).not_to have_content '/label ~bug'
+        expect(page).not_to have_content '/milestone %"ASAP"'
+        expect(page).to have_content 'Your commands have been executed!'
+
+        issuable.reload
+
+        expect(issuable.notes.user).to be_empty
+        expect(issuable.assignee).to eq assignee
+        expect(issuable.labels).to eq [label_bug]
+        expect(issuable.milestone).to eq milestone
+      end
+    end
+
+    context "with a note closing the #{issuable_type}" do
+      before do
+        expect(issuable).to be_open
+      end
+
+      context "when current user can close #{issuable_type}" do
+        it "closes the #{issuable_type}" do
+          write_note("/close")
+
+          expect(page).not_to have_content '/close'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          expect(issuable.reload).to be_closed
+        end
+      end
+
+      context "when current user cannot close #{issuable_type}" do
+        before do
+          logout
+          login_with(guest)
+          visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+        end
+
+        it "does not close the #{issuable_type}" do
+          write_note("/close")
+
+          expect(page).not_to have_content '/close'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          expect(issuable).to be_open
+        end
+      end
+    end
+
+    context "with a note reopening the #{issuable_type}" do
+      before do
+        issuable.close
+        expect(issuable).to be_closed
+      end
+
+      context "when current user can reopen #{issuable_type}" do
+        it "reopens the #{issuable_type}" do
+          write_note("/reopen")
+
+          expect(page).not_to have_content '/reopen'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          expect(issuable.reload).to be_open
+        end
+      end
+
+      context "when current user cannot reopen #{issuable_type}" do
+        before do
+          logout
+          login_with(guest)
+          visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+        end
+
+        it "does not reopen the #{issuable_type}" do
+          write_note("/reopen")
+
+          expect(page).not_to have_content '/reopen'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          expect(issuable).to be_closed
+        end
+      end
+    end
+
+    context "with a note changing the #{issuable_type}'s title" do
+      context "when current user can change title of #{issuable_type}" do
+        it "reopens the #{issuable_type}" do
+          write_note("/title Awesome new title")
+
+          expect(page).not_to have_content '/title'
+          expect(page).to have_content 'Your commands have been executed!'
+
+          expect(issuable.reload.title).to eq 'Awesome new title'
+        end
+      end
+
+      context "when current user cannot change title of #{issuable_type}" do
+        before do
+          logout
+          login_with(guest)
+          visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+        end
+
+        it "does not reopen the #{issuable_type}" do
+          write_note("/title Awesome new title")
+
+          expect(page).not_to have_content '/title'
+          expect(page).not_to have_content 'Your commands have been executed!'
+
+          expect(issuable.reload.title).not_to eq 'Awesome new title'
+        end
+      end
+    end
+
+    context "with a note marking the #{issuable_type} as todo" do
+      it "creates a new todo for the #{issuable_type}" do
+        write_note("/todo")
+
+        expect(page).not_to have_content '/todo'
+        expect(page).to have_content 'Your commands have been executed!'
+
+        todos = TodosFinder.new(master).execute
+        todo = todos.first
+
+        expect(todos.size).to eq 1
+        expect(todo).to be_pending
+        expect(todo.target).to eq issuable
+        expect(todo.author).to eq master
+        expect(todo.user).to eq master
+      end
+    end
+
+    context "with a note marking the #{issuable_type} as done" do
+      before do
+        TodoService.new.mark_todo(issuable, master)
+      end
+
+      it "creates a new todo for the #{issuable_type}" do
+        todos = TodosFinder.new(master).execute
+        todo = todos.first
+
+        expect(todos.size).to eq 1
+        expect(todos.first).to be_pending
+        expect(todo.target).to eq issuable
+        expect(todo.author).to eq master
+        expect(todo.user).to eq master
+
+        write_note("/done")
+
+        expect(page).not_to have_content '/done'
+        expect(page).to have_content 'Your commands have been executed!'
+
+        expect(todo.reload).to be_done
+      end
+    end
+
+    context "with a note subscribing to the #{issuable_type}" do
+      it "creates a new todo for the #{issuable_type}" do
+        expect(issuable.subscribed?(master)).to be_falsy
+
+        write_note("/subscribe")
+
+        expect(page).not_to have_content '/subscribe'
+        expect(page).to have_content 'Your commands have been executed!'
+
+        expect(issuable.subscribed?(master)).to be_truthy
+      end
+    end
+
+    context "with a note unsubscribing to the #{issuable_type} as done" do
+      before do
+        issuable.subscribe(master)
+      end
+
+      it "creates a new todo for the #{issuable_type}" do
+        expect(issuable.subscribed?(master)).to be_truthy
+
+        write_note("/unsubscribe")
+
+        expect(page).not_to have_content '/unsubscribe'
+        expect(page).to have_content 'Your commands have been executed!'
+
+        expect(issuable.subscribed?(master)).to be_falsy
+      end
+    end
+  end
+end
diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93422390ef72ef3a094997663d137c302159396d
--- /dev/null
+++ b/spec/support/git_helpers.rb
@@ -0,0 +1,9 @@
+module GitHelpers
+  def random_git_name
+    "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+  end
+end
+
+RSpec.configure do |config|
+  config.include GitHelpers
+end
diff --git a/spec/support/git_http_helpers.rb b/spec/support/git_http_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..46b686fce942ab7bc4e11a56774423f3bf5e37f5
--- /dev/null
+++ b/spec/support/git_http_helpers.rb
@@ -0,0 +1,48 @@
+module GitHttpHelpers
+  def clone_get(project, options = {})
+    get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+  end
+
+  def clone_post(project, options = {})
+    post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+  end
+
+  def push_get(project, options = {})
+    get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+  end
+
+  def push_post(project, options = {})
+    post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+  end
+
+  def download(project, user: nil, password: nil, spnego_request_token: nil)
+    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+    clone_get(*args)
+    yield response
+
+    clone_post(*args)
+    yield response
+  end
+
+  def upload(project, user: nil, password: nil, spnego_request_token: nil)
+    args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+    push_get(*args)
+    yield response
+
+    push_post(*args)
+    yield response
+  end
+
+  def auth_env(user, password, spnego_request_token)
+    env = workhorse_internal_api_request_header
+    if user && password
+      env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
+    elsif spnego_request_token
+      env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+    end
+
+    env
+  end
+end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2542a59bb00d019f0b4502e31e0c3193d4f32393
--- /dev/null
+++ b/spec/support/import_export/common_util.rb
@@ -0,0 +1,10 @@
+module ImportExport
+  module CommonUtil
+    def setup_symlink(tmpdir, symlink_name)
+      allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir)
+
+      File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
+      FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
+    end
+  end
+end
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f752508d48cfaef6cd801ca1f0cd7943d3be4625
--- /dev/null
+++ b/spec/support/import_export/configuration_helper.rb
@@ -0,0 +1,29 @@
+module ConfigurationHelper
+  # Returns a list of models from hashes/arrays contained in +project_tree+
+  def names_from_tree(project_tree)
+    project_tree.map do |branch_or_model|
+      branch_or_model =  branch_or_model.to_s if branch_or_model.is_a?(Symbol)
+
+      branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model)
+    end
+  end
+
+  def relation_class_for_name(relation_name)
+    relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
+    relation_name.to_s.classify.constantize
+  end
+
+  def parsed_attributes(relation_name, attributes)
+    excluded_attributes = config_hash['excluded_attributes'][relation_name]
+    included_attributes = config_hash['included_attributes'][relation_name]
+
+    attributes = attributes - JSON[excluded_attributes.to_json] if excluded_attributes
+    attributes = attributes & JSON[included_attributes.to_json] if included_attributes
+
+    attributes
+  end
+
+  def associations_for(safe_model)
+    safe_model.reflect_on_all_associations.map { |assoc| assoc.name.to_s }
+  end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b0a4583f5c148dc9540fe532e0e50381d7f32a4
--- /dev/null
+++ b/spec/support/import_export/export_file_helper.rb
@@ -0,0 +1,137 @@
+require './spec/support/import_export/configuration_helper'
+
+module ExportFileHelper
+  include ConfigurationHelper
+
+  ObjectWithParent = Struct.new(:object, :parent, :key_found)
+
+  def setup_project
+    project = create(:project, :public)
+
+    create(:release, project: project)
+
+    issue = create(:issue, assignee: user, project: project)
+    snippet = create(:project_snippet, project: project)
+    label = create(:label, project: project)
+    milestone = create(:milestone, project: project)
+    merge_request = create(:merge_request, source_project: project, milestone: milestone)
+    commit_status = create(:commit_status, project: project)
+
+    create(:label_link, label: label, target: issue)
+
+    ci_pipeline = create(:ci_pipeline,
+                         project: project,
+                         sha: merge_request.diff_head_sha,
+                         ref: merge_request.source_branch,
+                         statuses: [commit_status])
+
+    create(:ci_build, pipeline: ci_pipeline, project: project)
+    create(:milestone, project: project)
+    create(:note, noteable: issue, project: project)
+    create(:note, noteable: merge_request, project: project)
+    create(:note, noteable: snippet, project: project)
+    create(:note_on_commit,
+           author: user,
+           project: project,
+           commit_id: ci_pipeline.sha)
+
+    create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+    create(:project_member, :master, user: user, project: project)
+    create(:ci_variable, project: project)
+    create(:ci_trigger, project: project)
+    key = create(:deploy_key)
+    key.projects << project
+    create(:service, project: project)
+    create(:project_hook, project: project, token: 'token')
+    create(:protected_branch, project: project)
+
+    project
+  end
+
+  # Expands the compressed file for an exported project into +tmpdir+
+  def in_directory_with_expanded_export(project)
+    Dir.mktmpdir do |tmpdir|
+      export_file = project.export_project_path
+      _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+
+      yield(exit_status, tmpdir)
+    end
+  end
+
+  # Recursively finds key/values including +key+ as part of the key, inside a nested hash
+  def deep_find_with_parent(sensitive_key_word, object, found = nil)
+    sensitive_key_found = object_contains_key?(object, sensitive_key_word)
+
+    # Returns the parent object and the object found containing a sensitive word as part of the key
+    if sensitive_key_found && object[sensitive_key_found]
+      ObjectWithParent.new(object[sensitive_key_found], object, sensitive_key_found)
+    elsif object.is_a?(Enumerable)
+      # Recursively lookup for keys containing sensitive words in a Hash or Array
+      object_with_parent = nil
+
+      object.find do |*hash_or_array|
+        object_with_parent = deep_find_with_parent(sensitive_key_word, hash_or_array.last, found)
+      end
+
+      object_with_parent
+    end
+  end
+
+  # Return true if the hash has a key containing a sensitive word
+  def object_contains_key?(object, sensitive_key_word)
+    return false unless object.is_a?(Hash)
+
+    object.keys.find { |key| key.include?(sensitive_key_word) }
+  end
+
+  # Returns the offended ObjectWithParent object if a sensitive word is found inside a hash,
+  # excluding the whitelisted safe hashes.
+  def find_sensitive_attributes(sensitive_word, project_hash)
+    loop do
+      object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
+
+      return nil unless object_with_parent && object_with_parent.object
+
+      if is_safe_hash?(object_with_parent.parent, sensitive_word)
+        # It's in the safe list, remove hash and keep looking
+        object_with_parent.parent.delete(object_with_parent.key_found)
+      else
+        return object_with_parent
+      end
+
+      nil
+    end
+  end
+
+  # Returns true if it's one of the excluded models in +safe_list+
+  def is_safe_hash?(parent, sensitive_word)
+    return false unless parent && safe_list[sensitive_word.to_sym]
+
+    # Extra attributes that appear in a model but not in the exported hash.
+    excluded_attributes = ['type']
+
+    safe_list[sensitive_word.to_sym].each do |model|
+      # Check whether this is a hash attribute inside a model
+      if model.is_a?(Symbol)
+        return true if (safe_hashes[model] - parent.keys).empty?
+      else
+        return true if safe_model?(model, excluded_attributes, parent)
+      end
+    end
+
+    false
+  end
+
+  # Compares model attributes with those those found in the hash
+  # and returns true if there is a match, ignoring some excluded attributes.
+  def safe_model?(model, excluded_attributes, parent)
+    excluded_attributes += associations_for(model)
+    parsed_model_attributes = parsed_attributes(model.name.underscore, model.attribute_names)
+
+    (parsed_model_attributes - parent.keys - excluded_attributes).empty?
+  end
+
+  def file_permissions(file)
+    File.stat(file).mode & 0777
+  end
+end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb
index b6d7436c3609efa3d740c237ed4fa0704250ccd8..e70b3963d9d068dbf3598a71e054d19d24232aa3 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/issue_tracker_service_shared_example.rb
@@ -5,3 +5,18 @@ RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr|
   it { is_expected.not_to allow_value('ftp://example.com').for(url_attr) }
   it { is_expected.not_to allow_value('herp-and-derp').for(url_attr) }
 end
+
+RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
+  it 'allows underscores in the project name' do
+    expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+  end
+
+  it 'allows numbers in the project name' do
+    expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+  end
+
+  it 'requires the project name to begin with A-Z' do
+    expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+    expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+  end
+end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..adc3f48b43407c16dbb88b101b283a167baa124e
--- /dev/null
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -0,0 +1,45 @@
+require 'fileutils'
+require 'gitlab/popen'
+
+module JavaScriptFixturesHelpers
+  include Gitlab::Popen
+
+  FIXTURE_PATH = 'spec/javascripts/fixtures'
+
+  # Public: Removes all fixture files from given directory
+  #
+  # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
+  #
+  def clean_frontend_fixtures(directory_name)
+    directory_name = File.expand_path(directory_name, FIXTURE_PATH)
+    Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
+      FileUtils.rm(file_name)
+    end
+  end
+
+  # Public: Store a response object as fixture file
+  #
+  # response - response object to store
+  # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
+  #
+  def store_frontend_fixture(response, fixture_file_name)
+    fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+    fixture = response.body
+
+    response_mime_type = Mime::Type.lookup(response.content_type)
+    if response_mime_type.html?
+      doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+
+      scripts = doc.css('script')
+      scripts.remove
+
+      fixture = doc.to_html
+
+      # replace relative links
+      fixture.gsub!(%r{="/}, '="https://fixture.invalid/')
+    end
+
+    FileUtils.mkdir_p(File.dirname(fixture_file_name))
+    File.write(fixture_file_name, fixture)
+  end
+end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index f3ea206f387efd2ec26f472ccb3342eb4b6ddb66..96e0dad6b55383652ec76628ef147c961a0cade3 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,20 +1,17 @@
 module JiraServiceHelper
+  JIRA_URL = "http://jira.example.net"
+  JIRA_API = JIRA_URL + "/rest/api/2"
+
   def jira_service_settings
     properties = {
-      "title" => "JIRA tracker",
-      "project_url" => "http://jira.example/issues/?jql=project=A",
-      "issues_url" => "http://jira.example/browse/JIRA-1",
-      "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa",
-      "api_url" => "http://jira.example/rest/api/2"
+      title: "JIRA tracker",
+      url: JIRA_URL,
+      project_key: "JIRA"
     }
 
     jira_tracker.update_attributes(properties: properties, active: true)
   end
 
-  def jira_status_message
-    "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}."
-  end
-
   def jira_issue_comments
     "{\"startAt\":0,\"maxResults\":11,\"total\":11,
       \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
@@ -52,15 +49,32 @@ module JiraServiceHelper
       ]}"
   end
 
-  def jira_api_comment_url
-    'http://jira.example/rest/api/2/issue/JIRA-1/comment'
+  def jira_project_url
+    JIRA_API + "/project/#{jira_tracker.project_key}"
+  end
+
+  def jira_api_comment_url(issue_id)
+    JIRA_API + "/issue/#{issue_id}/comment"
   end
 
-  def jira_api_transition_url
-    'http://jira.example/rest/api/2/issue/JIRA-1/transitions'
+  def jira_api_transition_url(issue_id)
+    JIRA_API + "/issue/#{issue_id}/transitions"
   end
 
   def jira_api_test_url
-    'http://jira.example/rest/api/2/myself'
+    JIRA_API + "/myself"
+  end
+
+  def jira_issue_url(issue_id)
+    JIRA_API + "/issue/#{issue_id}"
+  end
+
+  def stub_jira_urls(issue_id)
+    WebMock.stub_request(:get, jira_project_url)
+    WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments)
+    WebMock.stub_request(:get, jira_issue_url(issue_id))
+    WebMock.stub_request(:get, jira_api_test_url)
+    WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+    WebMock.stub_request(:post, jira_api_transition_url(issue_id))
   end
 end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..079f244475cf721c37726ee74af57738c651e122
--- /dev/null
+++ b/spec/support/ldap_helpers.rb
@@ -0,0 +1,47 @@
+module LdapHelpers
+  def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
+    ::Gitlab::LDAP::Adapter.new(provider, ldap)
+  end
+
+  def user_dn(uid)
+    "uid=#{uid},ou=users,dc=example,dc=com"
+  end
+
+  # Accepts a hash of Gitlab::LDAP::Config keys and values.
+  #
+  # Example:
+  #   stub_ldap_config(
+  #     group_base: 'ou=groups,dc=example,dc=com',
+  #     admin_group: 'my-admin-group'
+  #   )
+  def stub_ldap_config(messages)
+    messages.each do |config, value|
+      allow_any_instance_of(::Gitlab::LDAP::Config)
+        .to receive(config.to_sym).and_return(value)
+    end
+  end
+
+  # Stub an LDAP person search and provide the return entry. Specify `nil` for
+  # `entry` to simulate when an LDAP person is not found
+  #
+  # Example:
+  #  adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap))
+  #  ldap_user_entry = ldap_user_entry('john_doe')
+  #
+  #  stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
+  def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
+    return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present?
+
+    allow(::Gitlab::LDAP::Person)
+      .to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
+  end
+
+  # Create a simple LDAP user entry.
+  def ldap_user_entry(uid)
+    entry = Net::LDAP::Entry.new
+    entry['dn'] = user_dn(uid)
+    entry['uid'] = uid
+
+    entry
+  end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index e5f76afbfc0883755c305d24a254355f32408e85..c0b3e83244ddd6fdca29dd7e77d8bba6629828d5 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,6 +75,7 @@ module LoginHelpers
   def logout
     find(".header-user-dropdown-toggle").click
     click_link "Sign out"
+    expect(page).to have_content('Signed out successfully')
   end
 
   # Logout without JavaScript driver
diff --git a/spec/support/matchers/be_like_time.rb b/spec/support/matchers/be_like_time.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f27390eab7569c338f4269161f7c4d07872a967
--- /dev/null
+++ b/spec/support/matchers/be_like_time.rb
@@ -0,0 +1,13 @@
+RSpec::Matchers.define :be_like_time do |expected|
+  match do |actual|
+    expect(actual).to be_within(1.second).of(expected)
+  end
+
+  description do
+    "within one second of #{expected}"
+  end
+
+  failure_message do |actual|
+    "expected #{actual} to be within one second of #{expected}"
+  end
+end
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02605d6b70e0bac8e0a6d1aa8b4e658b930d0fb3
--- /dev/null
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -0,0 +1,21 @@
+RSpec::Matchers.define :have_issuable_counts do |opts|
+  match do |actual|
+    expected_counts = opts.map do |state, count|
+      "#{state.to_s.humanize} #{count}"
+    end
+
+    actual.within '.issues-state-filters' do
+      expected_counts.each do |expected_count|
+        expect(actual).to have_content(expected_count)
+      end
+    end
+  end
+
+  description do
+    "displays the following issuable counts: #{expected_counts.inspect}"
+  end
+
+  failure_message do
+    "expected the following issuable counts: #{expected_counts.inspect} to be displayed"
+  end
+end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index e876d44c166dc384ebc21c8509638b7a532d88f0..f57c82809a6b878a65e528af75c7eb823d2ebd77 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
   let(:author)  { subject.author }
 
   let(:mentioned_issue)  { create(:issue, project: project) }
-  let!(:mentioned_mr)     { create(:merge_request, :simple, source_project: project) }
+  let!(:mentioned_mr)     { create(:merge_request, source_project: project) }
   let(:mentioned_commit) { project.commit("HEAD~1") }
 
   let(:ext_proj)   { create(:project, :public) }
@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
 
   it 'creates new cross-reference notes when the mentionable text is edited' do
     subject.save
+    subject.create_cross_references!
 
     new_text = <<-MSG.strip_heredoc
       These references already existed:
@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
     end
 
     # These two issues are new and should receive reference notes
+    # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
     new_issues.each do |newref|
       expect(SystemNoteService).to receive(:cross_reference).
         with(newref, subject.local_reference, author)
diff --git a/spec/mailers/shared/notify.rb b/spec/support/notify_shared_examples.rb
similarity index 91%
rename from spec/mailers/shared/notify.rb
rename to spec/support/notify_shared_examples.rb
index 93de5850ba28aa0cef1014a7102e9fe41d122ae4..49867aa5cc4dbdd0512d9b1d77c10928ee6d6d7e 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
   let(:new_user_address) { 'newguy@example.com' }
 
   before do
-    ActionMailer::Base.deliveries.clear
+    reset_delivered_emails!
     email = recipient.emails.create(email: "notifications@example.com")
     recipient.update_attribute(:notification_email, email.email)
     stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
@@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do
     reply_to = subject.header[:reply_to].addresses
     expect(reply_to).to eq([gitlab_sender_reply_to])
   end
+
+  context 'when custom suffix for email subject is set' do
+    before do
+      stub_config_setting(email_subject_suffix: 'A Nice Suffix')
+    end
+
+    it 'ends the subject with the suffix' do
+      is_expected.to have_subject /\ \| A Nice Suffix$/
+    end
+  end
 end
 
 shared_examples 'an email that contains a header with author username' do
@@ -169,10 +179,19 @@ shared_examples 'it should show Gmail Actions View Commit link' do
 end
 
 shared_examples 'an unsubscribeable thread' do
+  it 'has a List-Unsubscribe header in the correct format' do
+    is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+    is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
+  end
+
   it { is_expected.to have_body_text /unsubscribe/ }
 end
 
 shared_examples 'a user cannot unsubscribe through footer link' do
+  it 'does not have a List-Unsubscribe header' do
+    is_expected.not_to have_header 'List-Unsubscribe', /unsubscribe/
+  end
+
   it { is_expected.not_to have_body_text /unsubscribe/ }
 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
new file mode 100644
index 0000000000000000000000000000000000000000..4621d17549b9f3badc42e06235644f6628c046fd
--- /dev/null
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -0,0 +1,56 @@
+shared_examples 'project features apply to issuables' do |klass|
+  let(:described_class) { klass }
+
+  let(:group) { create(:group) }
+  let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+  let(:user_outside_group) { create(:user) }
+
+  let(:project) { create(:empty_project, :public, project_args) }
+
+  def project_args
+    feature = "#{described_class.model_name.plural}_access_level".to_sym
+
+    args = { group: group }
+    args[feature] = access_level
+
+    args
+  end
+
+  before do
+    _ = issuable
+    login_as(user)
+    visit path
+  end
+
+  context 'public access level' do
+    let(:access_level) { ProjectFeature::ENABLED }
+
+    context 'group member' do
+      let(:user) { user_in_group }
+
+      it { expect(page).to have_content(issuable.title) }
+    end
+
+    context 'non-member' do
+      let(:user) { user_outside_group }
+
+      it { expect(page).to have_content(issuable.title) }
+    end
+  end
+
+  context 'private access level' do
+    let(:access_level) { ProjectFeature::PRIVATE }
+
+    context 'group member' do
+      let(:user) { user_in_group }
+
+      it { expect(page).to have_content(issuable.title) }
+    end
+
+    context 'non-member' do
+      let(:user) { user_outside_group }
+
+      it { expect(page).not_to have_content(issuable.title) }
+    end
+  end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..52d80c698355e26c40b823bc0d1eb558b6ef079a
--- /dev/null
+++ b/spec/support/rake_helpers.rb
@@ -0,0 +1,10 @@
+module RakeHelpers
+  def run_rake_task(task_name)
+    Rake::Task[task_name].reenable
+    Rake.application.invoke_task task_name
+  end
+
+  def stub_warn_user_is_not_gitlab
+    allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
+  end
+end
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8eb74635a60babeea9a124e6520e587b48170e6c
--- /dev/null
+++ b/spec/support/reference_parser_shared_examples.rb
@@ -0,0 +1,43 @@
+RSpec.shared_examples "referenced feature visibility" do |*related_features|
+  let(:feature_fields) do
+    related_features.map { |feature| (feature + "_access_level").to_sym }
+  end
+
+  before { link['data-project'] = project.id.to_s }
+
+  context "when feature is disabled" do
+    it "does not create reference" do
+      set_features_fields_to(ProjectFeature::DISABLED)
+      expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+    end
+  end
+
+  context "when feature is enabled only for team members" do
+    before { set_features_fields_to(ProjectFeature::PRIVATE) }
+
+    it "does not create reference for non member" do
+      non_member = create(:user)
+
+      expect(subject.nodes_visible_to_user(non_member, [link])).to eq([])
+    end
+
+    it "creates reference for member" do
+      project.team << [user, :developer]
+
+      expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+    end
+  end
+
+  context "when feature is enabled" do
+    # The project is public
+    it "creates reference" do
+      set_features_fields_to(ProjectFeature::ENABLED)
+
+      expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+    end
+  end
+
+  def set_features_fields_to(visibility_level)
+    feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) }
+  end
+end
diff --git a/spec/support/search_helpers.rb b/spec/support/search_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..abbbb636d66c6607b10028065f6f869e7ad83003
--- /dev/null
+++ b/spec/support/search_helpers.rb
@@ -0,0 +1,5 @@
+module SearchHelpers
+  def select_filter(name)
+    find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
+  end
+end
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 35cc51725c65065a75067311f6edb81820df0400..d30cc8ff9f2955f8525559deab6d3020fffd3de1 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -17,9 +17,9 @@ module Select2Helper
     selector = options.fetch(:from)
 
     if options[:multiple]
-      execute_script("$('#{selector}').select2('val', ['#{value}'], true);")
+      execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
     else
-      execute_script("$('#{selector}').select2('val', '#{value}', true);")
+      execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
     end
   end
 end
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
new file mode 100644
index 0000000000000000000000000000000000000000..5f9645ed44f4c59aa0c98e90f5f060faa42d8498
--- /dev/null
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -0,0 +1,83 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It can take a `default_params`.
+
+shared_examples 'new issuable record that supports slash commands' do
+  let!(:project) { create(:project) }
+  let(:user) { create(:user).tap { |u| project.team << [u, :master] } }
+  let(:assignee) { create(:user) }
+  let!(:milestone) { create(:milestone, project: project) }
+  let!(:labels) { create_list(:label, 3, project: project) }
+  let(:base_params) { { title: FFaker::Lorem.sentence(3) } }
+  let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
+  let(:issuable) { described_class.new(project, user, params).execute }
+
+  context 'with labels in command only' do
+    let(:example_params) do
+      {
+        description: "/label ~#{labels.first.name} ~#{labels.second.name}\n/unlabel ~#{labels.third.name}"
+      }
+    end
+
+    it 'attaches labels to issuable' do
+      expect(issuable).to be_persisted
+      expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+    end
+  end
+
+  context 'with labels in params and command' do
+    let(:example_params) do
+      {
+        label_ids: [labels.second.id],
+        description: "/label ~#{labels.first.name}\n/unlabel ~#{labels.third.name}"
+      }
+    end
+
+    it 'attaches all labels to issuable' do
+      expect(issuable).to be_persisted
+      expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+    end
+  end
+
+  context 'with assignee and milestone in command only' do
+    let(:example_params) do
+      {
+        description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+      }
+    end
+
+    it 'assigns and sets milestone to issuable' do
+      expect(issuable).to be_persisted
+      expect(issuable.assignee).to eq(assignee)
+      expect(issuable.milestone).to eq(milestone)
+    end
+  end
+
+  context 'with assignee and milestone in params and command' do
+    let(:example_params) do
+      {
+        assignee: build_stubbed(:user),
+        milestone_id: double(:milestone),
+        description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+      }
+    end
+
+    it 'assigns and sets milestone to issuable from command' do
+      expect(issuable).to be_persisted
+      expect(issuable.assignee).to eq(assignee)
+      expect(issuable.milestone).to eq(milestone)
+    end
+  end
+
+  describe '/close' do
+    let(:example_params) do
+      {
+        description: '/close'
+      }
+    end
+
+    it 'returns an open issue' do
+      expect(issuable).to be_persisted
+      expect(issuable).to be_open
+    end
+  end
+end
diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..df483afa0e371cd870ecba7a2b9410d879af3442
--- /dev/null
+++ b/spec/support/slash_commands_helpers.rb
@@ -0,0 +1,10 @@
+module SlashCommandsHelpers
+  def write_note(text)
+    Sidekiq::Testing.fake! do
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: text
+        click_button 'Comment'
+      end
+    end
+  end
+end
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..57dfff3471ffca60c6066c4463b8e2cf80af68cc
--- /dev/null
+++ b/spec/support/snippets_shared_examples.rb
@@ -0,0 +1,18 @@
+# These shared examples expect a `snippets` array of snippets
+RSpec.shared_examples 'paginated snippets' do |remote: false|
+  it "is limited to #{Snippet.default_per_page} items per page" do
+    expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page)
+  end
+
+  context 'clicking on the link to the second page' do
+    before do
+      click_link('2')
+      wait_for_ajax if remote
+    end
+
+    it 'shows the remaining snippets' do
+      remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min
+      expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count)
+    end
+  end
+end
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 927c72c74098f000ea1cd0be098009ba79da13f4..ad1c783df4d917fc9c2460d02cd7f71f0cc69654 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -3,30 +3,63 @@
 # Requires a context containing:
 #   subject { Issue or MergeRequest }
 shared_examples 'a Taskable' do
-  before do
-    subject.description = <<-EOT.strip_heredoc
-      * [ ] Task 1
-      * [x] Task 2
-      * [x] Task 3
-      * [ ] Task 4
-      * [ ] Task 5
-    EOT
+  describe 'with multiple tasks' do
+    before do
+      subject.description = <<-EOT.strip_heredoc
+        * [ ] Task 1
+        * [x] Task 2
+        * [x] Task 3
+        * [ ] Task 4
+        * [ ] Task 5
+      EOT
+    end
+
+    it 'returns the correct task status' do
+      expect(subject.task_status).to match('2 of')
+      expect(subject.task_status).to match('5 tasks completed')
+      expect(subject.task_status_short).to match('2/')
+      expect(subject.task_status_short).to match('5 tasks')
+    end
+
+    describe '#tasks?' do
+      it 'returns true when object has tasks' do
+        expect(subject.tasks?).to eq true
+      end
+
+      it 'returns false when object has no tasks' do
+        subject.description = 'Now I have no tasks'
+        expect(subject.tasks?).to eq false
+      end
+    end
   end
 
-  it 'returns the correct task status' do
-    expect(subject.task_status).to match('5 tasks')
-    expect(subject.task_status).to match('2 completed')
-    expect(subject.task_status).to match('3 remaining')
+  describe 'with an incomplete task' do
+    before do
+      subject.description = <<-EOT.strip_heredoc
+        * [ ] Task 1
+      EOT
+    end
+
+    it 'returns the correct task status' do
+      expect(subject.task_status).to match('0 of')
+      expect(subject.task_status).to match('1 task completed')
+      expect(subject.task_status_short).to match('0/')
+      expect(subject.task_status_short).to match('1 task')
+    end
   end
 
-  describe '#tasks?' do
-    it 'returns true when object has tasks' do
-      expect(subject.tasks?).to eq true
+  describe 'with a complete task' do
+    before do
+      subject.description = <<-EOT.strip_heredoc
+        * [x] Task 1
+      EOT
     end
 
-    it 'returns false when object has no tasks' do
-      subject.description = 'Now I have no tasks'
-      expect(subject.tasks?).to eq false
+    it 'returns the correct task status' do
+      expect(subject.task_status).to match('1 of')
+      expect(subject.task_status).to match('1 task completed')
+      expect(subject.task_status_short).to match('1/')
+      expect(subject.task_status_short).to match('1 task')
     end
   end
 end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 1c0c66969e3bece8bd82183029bc4857a449c60f..778e665500d43c879c40713ca8a6cdde15069eb4 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,34 +5,45 @@ module TestEnv
 
   # When developing the seed repository, comment out the branch you will modify.
   BRANCH_SHA = {
-    'empty-branch'          => '7efb185',
-    'ends-with.json'        => '98b0d8b3',
-    'flatten-dir'           => 'e56497b',
-    'feature'               => '0b4bc9a',
-    'feature_conflict'      => 'bb5206f',
-    'fix'                   => '48f0be4',
-    'improve/awesome'       => '5937ac0',
-    'markdown'              => '0ed8c6c',
-    'lfs'                   => 'be93687',
-    'master'                => '5937ac0',
-    "'test'"                => 'e56497b',
-    'orphaned-branch'       => '45127a9',
-    'binary-encoding'       => '7b1cf43',
-    'gitattributes'         => '5a62481',
-    'expand-collapse-diffs' => '4842455',
-    'expand-collapse-files' => '025db92',
-    'expand-collapse-lines' => '238e82d',
-    'video'                 => '8879059',
-    'crlf-diff'             => '5938907'
+    'not-merged-branch'                  => 'b83d6e3',
+    'branch-merged'                      => '498214d',
+    'empty-branch'                       => '7efb185',
+    'ends-with.json'                     => '98b0d8b',
+    'flatten-dir'                        => 'e56497b',
+    'feature'                            => '0b4bc9a',
+    'feature_conflict'                   => 'bb5206f',
+    'fix'                                => '48f0be4',
+    'improve/awesome'                    => '5937ac0',
+    'markdown'                           => '0ed8c6c',
+    'lfs'                                => 'be93687',
+    'master'                             => 'b83d6e3',
+    'merge-test'                         => '5937ac0',
+    "'test'"                             => 'e56497b',
+    'orphaned-branch'                    => '45127a9',
+    'binary-encoding'                    => '7b1cf43',
+    'gitattributes'                      => '5a62481',
+    'expand-collapse-diffs'              => '4842455',
+    'expand-collapse-files'              => '025db92',
+    'expand-collapse-lines'              => '238e82d',
+    'video'                              => '8879059',
+    'crlf-diff'                          => '5938907',
+    'conflict-start'                     => '824be60',
+    'conflict-resolvable'                => '1450cd6',
+    'conflict-binary-file'               => '259a6fb',
+    'conflict-contains-conflict-markers' => '78a3086',
+    'conflict-missing-side'              => 'eb227b3',
+    'conflict-non-utf8'                  => 'd0a293c',
+    'conflict-too-large'                 => '39fa04f',
   }
 
   # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
   # need to keep all the branches in sync.
   # We currently only need a subset of the branches
   FORKED_BRANCH_SHA = {
-    'add-submodule-version-bump' => '3f547c08',
-    'master' => '5937ac0',
-    'remove-submodule' => '2a33e0c0'
+    'add-submodule-version-bump' => '3f547c0',
+    'master'                     => '5937ac0',
+    'remove-submodule'           => '2a33e0c',
+    'conflict-resolvable-fork'   => '404fa3f'
   }
 
   # Test environment
@@ -87,7 +98,9 @@ module TestEnv
 
   def setup_gitlab_shell
     unless File.directory?(Gitlab.config.gitlab_shell.path)
-      `rake gitlab:shell:install`
+      unless system('rake', 'gitlab:shell:install')
+        raise 'Can`t clone gitlab-shell'
+      end
     end
   end
 
@@ -110,22 +123,7 @@ module TestEnv
       system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
     end
 
-    Dir.chdir(repo_path) do
-      branch_sha.each do |branch, sha|
-        # Try to reset without fetching to avoid using the network.
-        reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
-        unless system(*reset)
-          if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
-            unless system(*reset)
-              raise 'The fetched test seed '\
-              'does not contain the required revision.'
-            end
-          else
-            raise 'Could not fetch test seed repository.'
-          end
-        end
-      end
-    end
+    set_repo_refs(repo_path, branch_sha)
 
     # We must copy bare repositories because we will push to them.
     system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
@@ -137,6 +135,7 @@ module TestEnv
     FileUtils.mkdir_p(target_repo_path)
     FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
     FileUtils.chmod_R 0755, target_repo_path
+    set_repo_refs(target_repo_path, BRANCH_SHA)
   end
 
   def repos_path
@@ -153,6 +152,7 @@ module TestEnv
     FileUtils.mkdir_p(target_repo_path)
     FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
     FileUtils.chmod_R 0755, target_repo_path
+    set_repo_refs(target_repo_path, FORKED_BRANCH_SHA)
   end
 
   # When no cached assets exist, manually hit the root path to create them
@@ -202,4 +202,21 @@ module TestEnv
   def git_env
     { 'GIT_TEMPLATE_DIR' => '' }
   end
+
+  def set_repo_refs(repo_path, branch_sha)
+    instructions = branch_sha.map {|branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
+    update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
+    reset = proc do
+      IO.popen(update_refs, "w") {|io| io.write(instructions) }
+      $?.success?
+    end
+
+    Dir.chdir(repo_path) do
+      # Try to reset without fetching to avoid using the network.
+      unless reset.call
+        raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
+        raise 'The fetched test seed does not contain the required revision.' unless reset.call
+      end
+    end
+  end
 end
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0c59a5c280e3c2bba2b9b24be7d17ac11c8145e
--- /dev/null
+++ b/spec/support/updating_mentions_shared_examples.rb
@@ -0,0 +1,32 @@
+RSpec.shared_examples 'updating mentions' do |service_class|
+  let(:mentioned_user) { create(:user) }
+  let(:service_class) { service_class }
+
+  before { project.team << [mentioned_user, :developer] }
+
+  def update_mentionable(opts)
+    reset_delivered_emails!
+
+    perform_enqueued_jobs do
+      service_class.new(project, user, opts).execute(mentionable)
+    end
+
+    mentionable.reload
+  end
+
+  context 'in title' do
+    before { update_mentionable(title: mentioned_user.to_reference) }
+
+    it 'emails only the newly-mentioned user' do
+      should_only_email(mentioned_user)
+    end
+  end
+
+  context 'in description' do
+    before { update_mentionable(description: mentioned_user.to_reference) }
+
+    it 'emails only the newly-mentioned user' do
+      should_only_email(mentioned_user)
+    end
+  end
+end
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index b90fc1126716d1511e5af745ef3e28026ce3bef6..0f9dc2dee754e5b6035187db8446e0a9a78e35d0 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -8,4 +8,8 @@ module WaitForAjax
   def finished_all_ajax_requests?
     page.evaluate_script('jQuery.active').zero?
   end
+
+  def javascript_test?
+    [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver)
+  end
 end
diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1029f84716fd99503eb95949ce7996292f2fced1
--- /dev/null
+++ b/spec/support/wait_for_vue_resource.rb
@@ -0,0 +1,7 @@
+module WaitForVueResource
+  def wait_for_vue_resource(spinner: true)
+    Timeout.timeout(Capybara.default_max_wait_time) do
+      loop until page.evaluate_script('Vue.activeResources').zero?
+    end
+  end
+end
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
index 107b6e309240d8f1b7165d445fd9e3c45cd70582..47673cd4c3afe1d0311f9a7c2b039c5ffcf07ab2 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/workhorse_helpers.rb
@@ -13,4 +13,9 @@ module WorkhorseHelpers
       ]
     end
   end
+
+  def workhorse_internal_api_request_header
+    jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+    { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
+  end
 end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 548e7780c362f5a6d3ba15536749e7ca76643a4f..287d83344db454f2f9c32ea776cc3afbd8832cd7 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -9,6 +9,7 @@ describe 'gitlab:app namespace rake task' do
     Rake.application.rake_require 'tasks/gitlab/backup'
     Rake.application.rake_require 'tasks/gitlab/shell'
     Rake.application.rake_require 'tasks/gitlab/db'
+    Rake.application.rake_require 'tasks/cache'
 
     # empty task as env is already loaded
     Rake::Task.define_task :environment
@@ -78,7 +79,7 @@ describe 'gitlab:app namespace rake task' do
     end
   end # backup_restore task
 
-  describe 'backup_create' do
+  describe 'backup' do
     def tars_glob
       Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
     end
@@ -97,6 +98,78 @@ describe 'gitlab:app namespace rake task' do
       @backup_tar = tars_glob.first
     end
 
+    def restore_backup
+      orig_stdout = $stdout
+      $stdout = StringIO.new
+      reenable_backup_sub_tasks
+      run_rake_task('gitlab:backup:restore')
+      reenable_backup_sub_tasks
+      $stdout = orig_stdout
+    end
+
+    describe 'backup creation and deletion using annex and custom_hooks' do
+      let(:project) { create(:project) }
+      let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
+
+      before(:each) do
+        @origin_cd = Dir.pwd
+
+        path = File.join(project.repository.path_to_repo, filename)
+        FileUtils.mkdir_p(path)
+        FileUtils.touch(File.join(path, "dummy.txt"))
+
+        # We need to use the full path instead of the relative one
+        allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(File.expand_path(Gitlab.config.gitlab_shell.path, Rails.root.to_s))
+
+        ENV["SKIP"] = "db"
+        create_backup
+      end
+
+      after(:each) do
+        ENV["SKIP"] = ""
+        FileUtils.rm(@backup_tar)
+        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" }
+
+        it 'creates custom_hooks.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}/custom_hooks.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, "custom_hooks"))).to include("dummy.txt")
+        end
+      end
+    end
+
     context 'tar creation' do
       before do
         create_backup
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..538ff952bf4aba04652c4e9808a42308a6e84993
--- /dev/null
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -0,0 +1,51 @@
+require 'rake_helper'
+
+describe 'gitlab:ldap:check rake task' do
+  include LdapHelpers
+
+  before do
+    Rake.application.rake_require 'tasks/gitlab/check'
+
+    stub_warn_user_is_not_gitlab
+  end
+
+  context 'when LDAP is not enabled' do
+    it 'does not attempt to bind or search for users' do
+      expect(Gitlab::LDAP::Config).not_to receive(:providers)
+      expect(Gitlab::LDAP::Adapter).not_to receive(:open)
+
+      run_rake_task('gitlab:ldap:check')
+    end
+  end
+
+  context 'when LDAP is enabled' do
+    let(:ldap) { double(:ldap) }
+    let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+    before do
+      allow(Gitlab::LDAP::Config)
+        .to receive_messages(
+          enabled?: true,
+          providers: ['ldapmain']
+        )
+      allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter)
+      allow(adapter).to receive(:users).and_return([])
+    end
+
+    it 'attempts to bind using credentials' do
+      stub_ldap_config(has_auth?: true)
+
+      expect(ldap).to receive(:bind)
+
+      run_rake_task('gitlab:ldap:check')
+    end
+
+    it 'searches for 100 LDAP users' do
+      stub_ldap_config(uid: 'uid')
+
+      expect(adapter).to receive(:users).with('uid', '*', 100)
+
+      run_rake_task('gitlab:ldap:check')
+    end
+  end
+end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..226d34fe2c98bc6a6539033c558be61e97469135
--- /dev/null
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -0,0 +1,26 @@
+require 'rake_helper'
+
+describe 'gitlab:shell rake tasks' do
+  before do
+    Rake.application.rake_require 'tasks/gitlab/shell'
+
+    stub_warn_user_is_not_gitlab
+  end
+
+  describe 'install task' do
+    it 'invokes create_hooks task' do
+      expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+
+      run_rake_task('gitlab:shell:install')
+    end
+  end
+
+  describe 'create_hooks task' do
+    it 'calls gitlab-shell bin/create_hooks' do
+      expect_any_instance_of(Object).to receive(:system)
+        .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args)
+
+      run_rake_task('gitlab:shell:create_hooks')
+    end
+  end
+end
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6ebef82b78d902571246e8600f1d18979a36cbe
--- /dev/null
+++ b/spec/tasks/gitlab/users_rake_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:users namespace rake task' do
+  let(:enable_registry) { true }
+
+  before :all do
+    Rake.application.rake_require 'tasks/gitlab/task_helpers'
+    Rake.application.rake_require 'tasks/gitlab/users'
+
+    # empty task as env is already loaded
+    Rake::Task.define_task :environment
+  end
+
+  def run_rake_task(task_name)
+    Rake::Task[task_name].reenable
+    Rake.application.invoke_task task_name
+  end
+
+  describe 'clear_all_authentication_tokens' do
+    before do
+      # avoid writing task output to spec progress
+      allow($stdout).to receive :write
+    end
+
+    context 'gitlab version' do
+      it 'clears the authentication token for all users' do
+        create_list(:user, 2)
+
+        expect(User.pluck(:authentication_token)).to all(be_present)
+
+        run_rake_task('gitlab:users:clear_all_authentication_tokens')
+
+        expect(User.pluck(:authentication_token)).to all(be_nil)
+      end
+    end
+  end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index dae858a52f6b3bf1f3bb7016698137360117b6b0..68d2d72876e50667e9f1561e340d8bf68efd37b7 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe 'admin/dashboard/index.html.haml' do
-  include Devise::TestHelpers
+  include Devise::Test::ControllerHelpers
 
   before do
     assign(:projects, create_list(:empty_project, 1))
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2dac5ee23c8de6728ec26e5d046bffccb43110cf
--- /dev/null
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'ci/lints/show' do
+  include Devise::TestHelpers
+
+  describe 'XSS protection' do
+    let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+    before do
+      assign(:status, true)
+      assign(:builds, config_processor.builds)
+      assign(:stages, config_processor.stages)
+      assign(:jobs, config_processor.jobs)
+    end
+
+    context 'when builds attrbiutes contain HTML nodes' do
+      let(:content) do
+        {
+          rspec: {
+            script: '<h1>rspec</h1>',
+            stage: 'test'
+          }
+        }
+      end
+
+      it 'does not render HTML elements' do
+        render
+
+        expect(rendered).not_to have_css('h1', text: 'rspec')
+      end
+    end
+
+    context 'when builds attributes do not contain HTML nodes' do
+      let(:content) do
+        {
+          rspec: {
+            script: 'rspec',
+            stage: 'test'
+          }
+        }
+      end
+
+      it 'shows configuration in the table' do
+        render
+
+        expect(rendered).to have_css('td pre', text: 'rspec')
+      end
+    end
+  end
+
+  let(:content) do
+    {
+      build_template: {
+        script: './build.sh',
+        tags: ['dotnet'],
+        only: ['test@dude/repo'],
+        except: ['deploy'],
+        environment: 'testing'
+      }
+    }
+  end
+
+  let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+
+  context 'when the content is valid' do
+    before do
+      assign(:status, true)
+      assign(:builds, config_processor.builds)
+      assign(:stages, config_processor.stages)
+      assign(:jobs, config_processor.jobs)
+    end
+
+    it 'shows the correct values' do
+      render
+
+      expect(rendered).to have_content('Tag list: dotnet')
+      expect(rendered).to have_content('Refs only: test@dude/repo')
+      expect(rendered).to have_content('Refs except: deploy')
+      expect(rendered).to have_content('Environment: testing')
+      expect(rendered).to have_content('When: on_success')
+    end
+  end
+
+  context 'when the content is invalid' do
+    before do
+      assign(:status, false)
+      assign(:error, 'Undefined error')
+    end
+
+    it 'shows error message' do
+      render
+
+      expect(rendered).to have_content('Status: syntax is incorrect')
+      expect(rendered).to have_content('Error: Undefined error')
+      expect(rendered).not_to have_content('Tag list:')
+    end
+  end
+end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index ee362e6fcb3225a5359ede8ada383cdc50ef06b1..1397bfa5864e8991f3b24a7fb8548467cb3435a1 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -12,13 +12,13 @@ describe 'devise/shared/_signin_box' do
 
       render
 
-      expect(rendered).to have_selector('#tab-crowd form')
+      expect(rendered).to have_selector('#crowd form')
     end
 
     it 'is not shown when Crowd is disabled' do
       render
 
-      expect(rendered).not_to have_selector('#tab-crowd')
+      expect(rendered).not_to have_selector('#crowd')
     end
   end
 
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3fddfb3b62f491b46f82fa30e03ca1809fe9bb80
--- /dev/null
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'layouts/_head' do
+  before do
+    stub_template 'layouts/_user_styles.html.haml' => ''
+  end
+
+  it 'escapes HTML-safe strings in page_title' do
+    stub_helper_with_safe_string(:page_title)
+
+    render
+
+    expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+  end
+
+  it 'escapes HTML-safe strings in page_description' do
+    stub_helper_with_safe_string(:page_description)
+
+    render
+
+    expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+  end
+
+  it 'escapes HTML-safe strings in page_image' do
+    stub_helper_with_safe_string(:page_image)
+
+    render
+
+    expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+  end
+
+  def stub_helper_with_safe_string(method)
+    allow_any_instance_of(PageLayoutHelper).to receive(method)
+      .and_return(%q{foo" http-equiv="refresh}.html_safe)
+  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 464051063d8903ba0d7e2ae6e54e48149b4d5d2a..da43622d3f94deca57cef095d162cd28f91cfcbc 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe 'projects/builds/show' do
-  include Devise::TestHelpers
+  include Devise::Test::ControllerHelpers
 
   let(:project) { create(:project) }
   let(:pipeline) do
@@ -59,14 +59,10 @@ describe 'projects/builds/show' do
     end
 
     it 'shows trigger variables in separate lines' do
-      expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1'))
-      expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2'))
+      expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1')
+      expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2')
+      expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1')
+      expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
     end
   end
-
-  private
-
-  def variable_regexp(key, value)
-    /\A#{Regexp.escape("#{key}=#{value}")}\Z/
-  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
new file mode 100644
index 0000000000000000000000000000000000000000..16bf0698c4be4b82646463aba56ac8a74757a458
--- /dev/null
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/commit/_commit_box.html.haml' do
+  include Devise::Test::ControllerHelpers
+
+  let(:project) { create(:project) }
+
+  before do
+    assign(:project, project)
+    assign(:commit, project.commit)
+  end
+
+  it 'shows the commit SHA' do
+    render
+
+    expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}")
+  end
+
+  it 'shows the last pipeline that ran for the commit' do
+    create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
+    create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
+    third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
+
+    render
+
+    expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
+  end
+end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 78af61f15a719e9a02f74d6cd57be4b8650f7bea..889d9a38887c9b4bb39a7424fbeb67b20b0b308f 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -1,11 +1,11 @@
 require 'spec_helper'
 
 describe 'projects/issues/_related_branches' do
-  include Devise::TestHelpers
+  include Devise::Test::ControllerHelpers
 
   let(:project) { create(:project) }
   let(:branch) { project.repository.find_branch('feature') }
-  let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') }
+  let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') }
 
   before do
     assign(:project, project)
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f70b3daf8e37fa2757df45b7decae40951fab9e
--- /dev/null
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/show/_commits.html.haml' do
+  include Devise::Test::ControllerHelpers
+
+  let(:user) { create(:user) }
+  let(:target_project) { create(:project) }
+
+  let(:source_project) do
+    create(:project, forked_from_project: target_project)
+  end
+
+  let(:merge_request) do
+    create(:merge_request, :simple,
+      source_project: source_project,
+      target_project: target_project,
+      author: user)
+  end
+
+  before do
+    controller.prepend_view_path('app/views/projects')
+
+    assign(:merge_request, merge_request)
+    assign(:commits, merge_request.commits)
+  end
+
+  it 'shows commits from source project' do
+    render
+
+    commit = source_project.commit(merge_request.source_branch)
+    href = namespace_project_commit_path(
+      source_project.namespace,
+      source_project,
+      commit)
+
+    expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href)
+  end
+end
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
deleted file mode 100644
index 733b2dfa7ffe9c30e7441b0714fe945112503175..0000000000000000000000000000000000000000
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/merge_requests/widget/_heading' do
-  include Devise::TestHelpers
-
-  context 'when released to an environment' do
-    let(:project)       { merge_request.target_project }
-    let(:merge_request) { create(:merge_request, :merged) }
-    let(:environment)   { create(:environment, project: project) }
-    let!(:deployment)   do
-      create(:deployment, environment: environment, sha: project.commit('master').id)
-    end
-
-    before do
-      assign(:merge_request, merge_request)
-      assign(:project, project)
-
-      render
-    end
-
-    it 'displays that the environment is deployed' do
-      expect(rendered).to match("Deployed to")
-      expect(rendered).to match("#{environment.name}")
-    end
-  end
-end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3650b22c389774a685025c159f5390c6070c765e
--- /dev/null
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/edit.html.haml' do
+  include Devise::Test::ControllerHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:fork_project) { create(:project, forked_from_project: project) }
+  let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+  let(:milestone) { create(:milestone, project: project) }
+
+  let(:closed_merge_request) do
+    create(:closed_merge_request,
+      source_project: fork_project,
+      target_project: project,
+      author: user,
+      assignee: user,
+      milestone: milestone)
+  end
+
+  before do
+    assign(:project, project)
+    assign(:merge_request, closed_merge_request)
+
+    allow(view).to receive(:can?).and_return(true)
+    allow(view).to receive(:current_user)
+      .and_return(User.find(closed_merge_request.author_id))
+  end
+
+  context 'when a merge request without fork' do
+    it "shows editable fields" do
+      unlink_project.execute
+      closed_merge_request.reload
+
+      render
+
+      expect(rendered).to have_field('merge_request[title]')
+      expect(rendered).to have_field('merge_request[description]')
+      expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+      expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+      expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
+    end
+  end
+
+  context 'when a merge request with an existing source project is closed' do
+    it "shows editable fields" do
+      render
+
+      expect(rendered).to have_field('merge_request[title]')
+      expect(rendered).to have_field('merge_request[description]')
+      expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+      expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+      expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
+    end
+  end
+end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..33cabd14913a0ee70c60a46cdc9e97db30db460b
--- /dev/null
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/show.html.haml' do
+  include Devise::Test::ControllerHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:fork_project) { create(:project, forked_from_project: project) }
+  let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+  let(:closed_merge_request) do
+    create(:closed_merge_request,
+      source_project: fork_project,
+      target_project: project,
+      author: user)
+  end
+
+  before do
+    assign(:project, project)
+    assign(:merge_request, closed_merge_request)
+    assign(:commits_count, 0)
+
+    allow(view).to receive(:can?).and_return(true)
+  end
+
+  context 'when the merge request is closed' do
+    it 'shows the "Reopen" button' do
+      render
+
+      expect(rendered).to have_css('a', visible: true, text: 'Reopen')
+      expect(rendered).to have_css('a', visible: false, text: 'Close')
+    end
+
+    it 'does not show the "Reopen" button when the source project does not exist' do
+      unlink_project.execute
+      closed_merge_request.reload
+
+      render
+
+      expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+      expect(rendered).to have_css('a', visible: false, text: 'Close')
+    end
+  end
+
+  context 'when the merge request is open' do
+    it 'closes the merge request if the source project does not exist' do
+      closed_merge_request.update_attributes(state: 'open')
+      fork_project.destroy
+
+      render
+
+      expect(closed_merge_request.reload.state).to eq('closed')
+      expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+      expect(rendered).to have_css('a', visible: false, text: 'Close')
+    end
+  end
+end
diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b14b1ece2d0c722e202d9ae80ed70506f0dda362
--- /dev/null
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/notes/_form' do
+  include Devise::Test::ControllerHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+
+  before do
+    project.team << [user, :master]
+    assign(:project, project)
+    assign(:note, note)
+
+    allow(view).to receive(:current_user).and_return(user)
+
+    render
+  end
+
+  %w[issue merge_request].each do |noteable|
+    context "with a note on #{noteable}" do
+      let(:note) { build(:"note_on_#{noteable}", project: project) }
+
+      it 'says that only markdown is supported, not slash commands' do
+        expect(rendered).to have_content('Styling with Markdown and slash commands are supported')
+      end
+    end
+  end
+
+  context 'with a note on a commit' do
+    let(:note) { build(:note_on_commit, project: project) }
+
+    it 'says that only markdown is supported, not slash commands' do
+      expect(rendered).to have_content('Styling with Markdown is supported')
+    end
+  end
+end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bf027499c94c59f6165510fea384070517f591a1
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/show' do
+  include Devise::Test::ControllerHelpers
+
+  let(:project) { create(:project) }
+  let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+
+  before do
+    controller.prepend_view_path('app/views/projects')
+
+    create_build('build', 0, 'build', :success)
+    create_build('test', 1, 'rspec 0:2', :pending)
+    create_build('test', 1, 'rspec 1:2', :running)
+    create_build('test', 1, 'spinach 0:2', :created)
+    create_build('test', 1, 'spinach 1:2', :created)
+    create_build('test', 1, 'audit', :created)
+    create_build('deploy', 2, 'production', :created)
+
+    create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+    assign(:project, project)
+    assign(:pipeline, pipeline)
+
+    allow(view).to receive(:can?).and_return(true)
+  end
+
+  it 'shows a graph with grouped stages' do
+    render
+
+    expect(rendered).to have_css('.pipeline-graph')
+    expect(rendered).to have_css('.grouped-pipeline-dropdown')
+
+    # stages
+    expect(rendered).to have_text('Build')
+    expect(rendered).to have_text('Test')
+    expect(rendered).to have_text('Deploy')
+    expect(rendered).to have_text('External')
+
+    # builds
+    expect(rendered).to have_text('rspec')
+    expect(rendered).to have_text('spinach')
+    expect(rendered).to have_text('rspec 0:2')
+    expect(rendered).to have_text('production')
+    expect(rendered).to have_text('jenkins')
+  end
+
+  private
+
+  def create_build(stage, stage_idx, name, status)
+    create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
+  end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 0f3fc1ee1ac2b2f831cb76ae0d2d00e9fa504444..c381b1a86dfeb7e123fe4389aad0e69b8604cc09 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe 'projects/tree/show' do
-  include Devise::TestHelpers
+  include Devise::Test::ControllerHelpers
 
   let(:project) { create(:project) }
   let(:repository) { project.repository }
diff --git a/spec/workers/build_coverage_worker_spec.rb b/spec/workers/build_coverage_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba20488f66343b2dfba8d18145986819bb6621ef
--- /dev/null
+++ b/spec/workers/build_coverage_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildCoverageWorker do
+  describe '#perform' do
+    context 'when build exists' do
+      let!(:build) { create(:ci_build) }
+
+      it 'updates code coverage' do
+        expect_any_instance_of(Ci::Build)
+          .to receive(:update_coverage)
+
+        described_class.new.perform(build.id)
+      end
+    end
+
+    context 'when build does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 788b92c1b84e6e36a71e3416fb21797706713297..a1aa336361a3166fbb9424146b185086e897d757 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -24,7 +24,7 @@ describe BuildEmailWorker do
     end
 
     it "gracefully handles an input SMTP error" do
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
       allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
 
       subject.perform(build.id, [user.email], data.stringify_keys)
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2868167c7d41917c39172a051f777c3076faaa63
--- /dev/null
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe BuildFinishedWorker do
+  describe '#perform' do
+    context 'when build exists' do
+      let(:build) { create(:ci_build) }
+
+      it 'calculates coverage and calls hooks' do
+        expect(BuildCoverageWorker)
+          .to receive(:new).ordered.and_call_original
+        expect(BuildHooksWorker)
+          .to receive(:new).ordered.and_call_original
+
+        expect_any_instance_of(BuildCoverageWorker)
+          .to receive(:perform)
+        expect_any_instance_of(BuildHooksWorker)
+          .to receive(:perform)
+
+        described_class.new.perform(build.id)
+      end
+    end
+
+    context 'when build does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..97654a93f5c51f5b89708f0a4d3f0c849f16d102
--- /dev/null
+++ b/spec/workers/build_hooks_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildHooksWorker do
+  describe '#perform' do
+    context 'when build exists' do
+      let!(:build) { create(:ci_build) }
+
+      it 'calls build hooks' do
+        expect_any_instance_of(Ci::Build)
+          .to receive(:execute_hooks)
+
+        described_class.new.perform(build.id)
+      end
+    end
+
+    context 'when build does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dba7088313093ecd51217930c9c33ed86a5f3679
--- /dev/null
+++ b/spec/workers/build_success_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe BuildSuccessWorker do
+  describe '#perform' do
+    context 'when build exists' do
+      context 'when build belogs to the environment' do
+        let!(:build) { create(:ci_build, environment: 'production') }
+
+        it 'executes deployment service' do
+          expect_any_instance_of(CreateDeploymentService)
+            .to receive(:execute)
+
+          described_class.new.perform(build.id)
+        end
+      end
+
+      context 'when build is not associated with project' do
+        let!(:build) { create(:ci_build, project: nil) }
+
+        it 'does not create deployment' do
+          expect_any_instance_of(CreateDeploymentService)
+            .not_to receive(:execute)
+
+          described_class.new.perform(build.id)
+        end
+      end
+    end
+
+    context 'when build does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6bf955e0be22c9f971458ca815b8ed303ef798cc
--- /dev/null
+++ b/spec/workers/concerns/build_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe BuildQueue do
+  let(:worker) do
+    Class.new do
+      include Sidekiq::Worker
+      include BuildQueue
+    end
+  end
+
+  it 'sets the queue name of a worker' do
+    expect(worker.sidekiq_options['queue'].to_s).to eq('build')
+  end
+end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d1336c21a6b71c5def5d4ebab873cdddcfb05ae
--- /dev/null
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe CronjobQueue do
+  let(:worker) do
+    Class.new do
+      include Sidekiq::Worker
+      include CronjobQueue
+    end
+  end
+
+  it 'sets the queue name of a worker' do
+    expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+  end
+
+  it 'disables retrying of failed jobs' do
+    expect(worker.sidekiq_options['retry']).to eq(false)
+  end
+end
diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..512baec8b7e1de795f544fb75603a3b138a7e1a7
--- /dev/null
+++ b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DedicatedSidekiqQueue do
+  let(:worker) do
+    Class.new do
+      def self.name
+        'Foo::Bar::DummyWorker'
+      end
+
+      include Sidekiq::Worker
+      include DedicatedSidekiqQueue
+    end
+  end
+
+  describe 'queue names' do
+    it 'sets the queue name based on the class name' do
+      expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
+    end
+  end
+end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..40794d0e42a8d9ea97f0bce3f92415ad7c7743b8
--- /dev/null
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe PipelineQueue do
+  let(:worker) do
+    Class.new do
+      include Sidekiq::Worker
+      include PipelineQueue
+    end
+  end
+
+  it 'sets the queue name of a worker' do
+    expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline')
+  end
+end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8868e9698293a82ed2162cf06261db4cca1189c8
--- /dev/null
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe RepositoryCheckQueue do
+  let(:worker) do
+    Class.new do
+      include Sidekiq::Worker
+      include RepositoryCheckQueue
+    end
+  end
+
+  it 'sets the queue name of a worker' do
+    expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check')
+  end
+
+  it 'disables retrying of failed jobs' do
+    expect(worker.sidekiq_options['retry']).to eq(false)
+  end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index eecc32875a591fc19e5f3f43dca4be4a8d0ee656..fc652f6f4c36cb4180eb6ce69e4e837bbb24fc30 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,19 +2,19 @@ require 'spec_helper'
 
 describe EmailsOnPushWorker do
   include RepoHelpers
+  include EmailSpec::Matchers
 
   let(:project) { create(:project) }
   let(:user) { create(:user) }
   let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
   let(:recipients) { user.email }
   let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+  let(:email) { ActionMailer::Base.deliveries.last }
 
   subject { EmailsOnPushWorker.new }
 
   describe "#perform" do
     context "when push is a new branch" do
-      let(:email) { ActionMailer::Base.deliveries.last }
-
       before do
         data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
 
@@ -31,8 +31,6 @@ describe EmailsOnPushWorker do
     end
 
     context "when push is a deleted branch" do
-      let(:email) { ActionMailer::Base.deliveries.last }
-
       before do
         data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
 
@@ -48,13 +46,38 @@ describe EmailsOnPushWorker do
       end
     end
 
-    context "when there are no errors in sending" do
-      let(:email) { ActionMailer::Base.deliveries.last }
+    context "when push is a force push to delete commits" do
+      before do
+        data_force_push = data.stringify_keys.merge(
+          "after"  => data[:before],
+          "before" => data[:after]
+        )
+
+        subject.perform(project.id, recipients, data_force_push)
+      end
 
+      it "sends a mail with the correct subject" do
+        expect(email.subject).to include('adds bar folder and branch-test text file')
+      end
+
+      it "mentions force pushing in the body" do
+        expect(email).to have_body_text("force push")
+      end
+
+      it "sends the mail to the correct recipient" do
+        expect(email.to).to eq([user.email])
+      end
+    end
+
+    context "when there are no errors in sending" do
       before { perform }
 
       it "sends a mail with the correct subject" do
-        expect(email.subject).to include('Change some files')
+        expect(email.subject).to include('adds bar folder and branch-test text file')
+      end
+
+      it "does not mention force pushing in the body" do
+        expect(email).not_to have_body_text("force push")
       end
 
       it "sends the mail to the correct recipient" do
@@ -64,8 +87,9 @@ describe EmailsOnPushWorker do
 
     context "when there is an SMTP error" do
       before do
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
         allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+        allow(subject).to receive_message_chain(:logger, :info)
         perform
       end
 
@@ -88,7 +112,7 @@ describe EmailsOnPushWorker do
           original.call(Mail.new(mail.encoded))
         end
 
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       it "sends the mail to each of the recipients" do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fc9adf47c1e833ee529bea87e3666dff8d16a9ac
--- /dev/null
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Every Sidekiq worker' do
+  let(:workers) do
+    root = Rails.root.join('app', 'workers')
+    concerns = root.join('concerns').to_s
+
+    workers = Dir[root.join('**', '*.rb')].
+      reject { |path| path.start_with?(concerns) }
+
+    workers.map do |path|
+      ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
+
+      ns.camelize.constantize
+    end
+  end
+
+  it 'does not use the default queue' do
+    workers.each do |worker|
+      expect(worker.sidekiq_options['queue'].to_s).not_to eq('default')
+    end
+  end
+
+  it 'uses the cronjob queue when the worker runs as a cronjob' do
+    cron_workers = Settings.cron_jobs.
+      map { |job_name, options| options['job_class'].constantize }.
+      to_set
+
+    workers.each do |worker|
+      next unless cron_workers.include?(worker)
+
+      expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+    end
+  end
+
+  it 'defines the queue in the Sidekiq configuration file' do
+    config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+    queue_names = config[:queues].map { |(queue, _)| queue }.to_set
+
+    workers.each do |worker|
+      expect(queue_names).to include(worker.sidekiq_options['queue'].to_s)
+    end
+  end
+end
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 7d6668920c079d75c3b12c7709f6ed47d96c98de..73cbadc13d910399296a52bbed999525815f7caf 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -5,65 +5,42 @@ describe ExpireBuildArtifactsWorker do
 
   let(:worker) { described_class.new }
 
+  before { Sidekiq::Worker.clear_all }
+
   describe '#perform' do
     before { build }
 
-    subject! { worker.perform }
+    subject! do
+      Sidekiq::Testing.fake! { worker.perform }
+    end
 
     context 'with expired artifacts' do
       let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
 
-      it 'does expire' do
-        expect(build.reload.artifacts_expired?).to be_truthy
-      end
-
-      it 'does remove files' do
-        expect(build.reload.artifacts_file.exists?).to be_falsey
-      end
-
-      it 'does nullify artifacts_file column' do
-        expect(build.reload.artifacts_file_identifier).to be_nil
+      it 'enqueues that build' do
+        expect(jobs_enqueued.size).to eq(1)
+        expect(jobs_enqueued[0]["args"]).to eq([build.id])
       end
     end
 
     context 'with not yet expired artifacts' do
       let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
 
-      it 'does not expire' do
-        expect(build.reload.artifacts_expired?).to be_falsey
-      end
-
-      it 'does not remove files' do
-        expect(build.reload.artifacts_file.exists?).to be_truthy
-      end
-
-      it 'does not nullify artifacts_file column' do
-        expect(build.reload.artifacts_file_identifier).not_to be_nil
+      it 'does not enqueue that build' do
+        expect(jobs_enqueued.size).to eq(0)
       end
     end
 
     context 'without expire date' do
       let(:build) { create(:ci_build, :artifacts) }
 
-      it 'does not expire' do
-        expect(build.reload.artifacts_expired?).to be_falsey
-      end
-
-      it 'does not remove files' do
-        expect(build.reload.artifacts_file.exists?).to be_truthy
-      end
-
-      it 'does not nullify artifacts_file column' do
-        expect(build.reload.artifacts_file_identifier).not_to be_nil
+      it 'does not enqueue that build' do
+        expect(jobs_enqueued.size).to eq(0)
       end
     end
 
-    context 'for expired artifacts' do
-      let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
-
-      it 'is still expired' do
-        expect(build.reload.artifacts_expired?).to be_truthy
-      end
+    def jobs_enqueued
+      Sidekiq::Queues.jobs_by_worker['ExpireBuildInstanceArtifactsWorker']
     end
   end
 end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d202b3de77edc8d37d3d57d24cb2b99feffd48c5
--- /dev/null
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe ExpireBuildInstanceArtifactsWorker do
+  include RepoHelpers
+
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    before do
+      worker.perform(build.id)
+    end
+
+    context 'with expired artifacts' do
+      let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } }
+
+      context 'when associated project is valid' do
+        let(:build) do
+          create(:ci_build, :artifacts, artifacts_expiry)
+        end
+
+        it 'does expire' do
+          expect(build.reload.artifacts_expired?).to be_truthy
+        end
+
+        it 'does remove files' do
+          expect(build.reload.artifacts_file.exists?).to be_falsey
+        end
+
+        it 'does nullify artifacts_file column' do
+          expect(build.reload.artifacts_file_identifier).to be_nil
+        end
+      end
+
+      context 'when associated project was removed' do
+        let(:build) do
+          create(:ci_build, :artifacts, artifacts_expiry) do |build|
+            build.project.delete
+          end
+        end
+
+        it 'does not remove artifacts' do
+          expect(build.reload.artifacts_file.exists?).to be_truthy
+        end
+      end
+    end
+
+    context 'with not yet expired artifacts' do
+      let(:build) do
+        create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days)
+      end
+
+      it 'does not expire' do
+        expect(build.reload.artifacts_expired?).to be_falsey
+      end
+
+      it 'does not remove files' do
+        expect(build.reload.artifacts_file.exists?).to be_truthy
+      end
+
+      it 'does not nullify artifacts_file column' do
+        expect(build.reload.artifacts_file_identifier).not_to be_nil
+      end
+    end
+
+    context 'without expire date' do
+      let(:build) { create(:ci_build, :artifacts) }
+
+      it 'does not expire' do
+        expect(build.reload.artifacts_expired?).to be_falsey
+      end
+
+      it 'does not remove files' do
+        expect(build.reload.artifacts_file.exists?).to be_truthy
+      end
+
+      it 'does not nullify artifacts_file column' do
+        expect(build.reload.artifacts_file_identifier).not_to be_nil
+      end
+    end
+
+    context 'for expired artifacts' do
+      let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+      it 'is still expired' do
+        expect(build.reload.artifacts_expired?).to be_truthy
+      end
+    end
+  end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index c9f5aae0815b364f883060d8fcee62c67bd9a360..e471a68a49afeb0bfe17e1e7f2b032dfb967f6f7 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -1,3 +1,5 @@
+require 'fileutils'
+
 require 'spec_helper'
 
 describe GitGarbageCollectWorker do
@@ -6,16 +8,12 @@ describe GitGarbageCollectWorker do
 
   subject { GitGarbageCollectWorker.new }
 
-  before do
-    allow(subject).to receive(:gitlab_shell).and_return(shell)
-  end
-
   describe "#perform" do
-    it "runs `git gc`" do
-      expect(shell).to receive(:gc).with(
-        project.repository_storage_path,
-        project.path_with_namespace).
-      and_return(true)
+    it "flushes ref caches when the task is 'gc'" do
+      expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
+      expect(Gitlab::Popen).to receive(:popen).
+        with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+
       expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
       expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
       expect_any_instance_of(Repository).to receive(:branch_count).and_call_original
@@ -23,5 +21,110 @@ describe GitGarbageCollectWorker do
 
       subject.perform(project.id)
     end
+
+    shared_examples 'gc tasks' do
+      before { allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) }
+
+      it 'incremental repack adds a new packfile' do
+        create_objects(project)
+        before_packs = packs(project)
+
+        expect(before_packs.count).to be >= 1
+
+        subject.perform(project.id, 'incremental_repack')
+        after_packs = packs(project)
+
+        # Exactly one new pack should have been created
+        expect(after_packs.count).to eq(before_packs.count + 1)
+
+        # Previously existing packs are still around
+        expect(before_packs & after_packs).to eq(before_packs)
+      end
+
+      it 'full repack consolidates into 1 packfile' do
+        create_objects(project)
+        subject.perform(project.id, 'incremental_repack')
+        before_packs = packs(project)
+
+        expect(before_packs.count).to be >= 2
+
+        subject.perform(project.id, 'full_repack')
+        after_packs = packs(project)
+
+        expect(after_packs.count).to eq(1)
+
+        # Previously existing packs should be gone now
+        expect(after_packs - before_packs).to eq(after_packs)
+
+        expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+      end
+
+      it 'gc consolidates into 1 packfile and updates packed-refs' do
+        create_objects(project)
+        before_packs = packs(project)
+        before_packed_refs = packed_refs(project)
+
+        expect(before_packs.count).to be >= 1
+
+        subject.perform(project.id, 'gc')
+        after_packed_refs = packed_refs(project)
+        after_packs = packs(project)
+
+        expect(after_packs.count).to eq(1)
+
+        # Previously existing packs should be gone now
+        expect(after_packs - before_packs).to eq(after_packs)
+
+        # The packed-refs file should have been updated during 'git gc'
+        expect(before_packed_refs).not_to eq(after_packed_refs)
+
+        expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+      end
+    end
+
+    context 'with bitmaps enabled' do
+      let(:bitmaps_enabled) { true }
+
+      include_examples 'gc tasks'
+    end
+
+    context 'with bitmaps disabled' do
+      let(:bitmaps_enabled) { false }
+
+      include_examples 'gc tasks'
+    end
+  end
+
+  # Create a new commit on a random new branch
+  def create_objects(project)
+    rugged = project.repository.rugged
+    old_commit = rugged.branches.first.target
+    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'),
+      tree: old_commit.tree,
+      parents: [old_commit],
+    )
+    project.repository.update_ref!(
+      "refs/heads/#{SecureRandom.hex(6)}",
+      new_commit_sha,
+      Gitlab::Git::BLANK_SHA
+    )
+  end
+
+  def packs(project)
+    Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"]
+  end
+
+  def packed_refs(project)
+    path = "#{project.repository.path_to_repo}/packed-refs"
+    FileUtils.touch(path)
+    File.read(path)
+  end
+
+  def bitmap_path(pack)
+    pack.sub(/\.pack\z/, '.bitmap')
   end
 end
diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..035e329839fd7a5f8f910b5118d9f88c895c45ad
--- /dev/null
+++ b/spec/workers/pipeline_hooks_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe PipelineHooksWorker do
+  describe '#perform' do
+    context 'when pipeline exists' do
+      let(:pipeline) { create(:ci_pipeline) }
+
+      it 'executes hooks for the pipeline' do
+        expect_any_instance_of(Ci::Pipeline)
+          .to receive(:execute_hooks)
+
+        described_class.new.perform(pipeline.id)
+      end
+    end
+
+    context 'when pipeline does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c9e7c2cd023609d56857866be903d6fe4b7a0fb
--- /dev/null
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe PipelineMetricsWorker do
+  let(:project) { create(:project) }
+  let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+  let(:pipeline) do
+    create(:ci_empty_pipeline,
+           status: status,
+           project: project,
+           ref: 'master',
+           sha: project.repository.commit('master').id,
+           started_at: 1.hour.ago,
+           finished_at: Time.now)
+  end
+
+  describe '#perform' do
+    subject { described_class.new.perform(pipeline.id) }
+
+    context 'when pipeline is running' do
+      let(:status) { 'running' }
+
+      it 'records the build start time' do
+        subject
+
+        expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
+      end
+
+      it 'clears the build end time' do
+        subject
+
+        expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+      end
+    end
+
+    context 'when pipeline succeeded' do
+      let(:status) { 'success' }
+
+      it 'records the build end time' do
+        subject
+
+        expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
+      end
+    end
+  end
+end
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d487a7196800fc7298fb547fd03f6bd4a42aeb43
--- /dev/null
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -0,0 +1,131 @@
+require 'spec_helper'
+
+describe PipelineNotificationWorker do
+  let(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit('master').sha,
+           user: pusher,
+           status: status)
+  end
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:pusher) { user }
+  let(:watcher) { pusher }
+
+  describe '#execute' do
+    before do
+      reset_delivered_emails!
+      pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
+    end
+
+    context 'when watcher has developer access' do
+      before do
+        pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
+      end
+
+      shared_examples 'sending emails' do
+        it 'sends emails' do
+          perform_enqueued_jobs do
+            subject.perform(pipeline.id)
+          end
+
+          emails = ActionMailer::Base.deliveries
+          actual = emails.flat_map(&:bcc).sort
+          expected_receivers = receivers.map(&:email).uniq.sort
+
+          expect(actual).to eq(expected_receivers)
+          expect(emails.size).to eq(1)
+          expect(emails.last.subject).to include(email_subject)
+        end
+      end
+
+      context 'with success pipeline' do
+        let(:status) { 'success' }
+        let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
+        let(:receivers) { [pusher, watcher] }
+
+        it_behaves_like 'sending emails'
+
+        context 'with pipeline from someone else' do
+          let(:pusher) { create(:user) }
+          let(:watcher) { user }
+
+          context 'with success pipeline notification on' do
+            before do
+              watcher.global_notification_setting.
+                update(level: 'custom', success_pipeline: true)
+            end
+
+            it_behaves_like 'sending emails'
+          end
+
+          context 'with success pipeline notification off' do
+            let(:receivers) { [pusher] }
+
+            before do
+              watcher.global_notification_setting.
+                update(level: 'custom', success_pipeline: false)
+            end
+
+            it_behaves_like 'sending emails'
+          end
+        end
+
+        context 'with failed pipeline' do
+          let(:status) { 'failed' }
+          let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+
+          it_behaves_like 'sending emails'
+
+          context 'with pipeline from someone else' do
+            let(:pusher) { create(:user) }
+            let(:watcher) { user }
+
+            context 'with failed pipeline notification on' do
+              before do
+                watcher.global_notification_setting.
+                  update(level: 'custom', failed_pipeline: true)
+              end
+
+              it_behaves_like 'sending emails'
+            end
+
+            context 'with failed pipeline notification off' do
+              let(:receivers) { [pusher] }
+
+              before do
+                watcher.global_notification_setting.
+                  update(level: 'custom', failed_pipeline: false)
+              end
+
+              it_behaves_like 'sending emails'
+            end
+          end
+        end
+      end
+    end
+
+    context 'when watcher has no read_build access' do
+      let(:status) { 'failed' }
+      let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+      let(:watcher) { create(:user) }
+
+      before do
+        pipeline.project.team << [watcher, Gitlab::Access::GUEST]
+
+        watcher.global_notification_setting.
+          update(level: 'custom', failed_pipeline: true)
+
+        perform_enqueued_jobs do
+          subject.perform(pipeline.id)
+        end
+      end
+
+      it 'does not send emails' do
+        should_only_email(pusher, kind: :bcc)
+      end
+    end
+  end
+end
diff --git a/spec/workers/pipeline_proccess_worker_spec.rb b/spec/workers/pipeline_proccess_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86e9d7f6684080813747f6e111ebf211a2778fb3
--- /dev/null
+++ b/spec/workers/pipeline_proccess_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PipelineProcessWorker do
+  describe '#perform' do
+    context 'when pipeline exists' do
+      let(:pipeline) { create(:ci_pipeline) }
+
+      it 'processes pipeline' do
+        expect_any_instance_of(Ci::Pipeline).to receive(:process!)
+
+        described_class.new.perform(pipeline.id)
+      end
+    end
+
+    context 'when pipeline does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e31cc2c8e7754ccc2b61fb032ef0c62f6c756cc
--- /dev/null
+++ b/spec/workers/pipeline_success_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PipelineSuccessWorker do
+  describe '#perform' do
+    context 'when pipeline exists' do
+      let(:pipeline) { create(:ci_pipeline, status: 'success') }
+
+      it 'performs "merge when pipeline succeeds"' do
+        expect_any_instance_of(
+          MergeRequests::MergeWhenBuildSucceedsService
+        ).to receive(:trigger)
+
+        described_class.new.perform(pipeline.id)
+      end
+    end
+
+    context 'when pipeline does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/pipeline_update_worker_spec.rb b/spec/workers/pipeline_update_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0b456cfd0dac10e7a60d8bb6ca2035e967e20049
--- /dev/null
+++ b/spec/workers/pipeline_update_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PipelineUpdateWorker do
+  describe '#perform' do
+    context 'when pipeline exists' do
+      let(:pipeline) { create(:ci_pipeline) }
+
+      it 'updates pipeline status' do
+        expect_any_instance_of(Ci::Pipeline).to receive(:update_status)
+
+        described_class.new.perform(pipeline.id)
+      end
+    end
+
+    context 'when pipeline does not exist' do
+      it 'does not raise exception' do
+        expect { described_class.new.perform(123) }
+          .not_to raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 1d2cf7acddd2ca6dd3bd9aa6465786a62fa71c65..984acdade3603c1964a5ed7b28c63267047d3ab7 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -79,7 +79,9 @@ describe PostReceive do
     end
 
     it "does not run if the author is not in the project" do
-      allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil }
+      allow_any_instance_of(Gitlab::GitPostReceive).
+        to receive(:identify_using_ssh_key).
+        and_return(nil)
 
       expect(project).not_to receive(:execute_hooks)
 
@@ -90,7 +92,13 @@ describe PostReceive do
       allow(Project).to receive(:find_with_namespace).and_return(project)
       expect(project).to receive(:execute_hooks).twice
       expect(project).to receive(:execute_services).twice
-      expect(project).to receive(:update_merge_requests)
+
+      PostReceive.new.perform(pwd(project), key_id, base64_changes)
+    end
+
+    it "enqueues a UpdateMergeRequestsWorker job" do
+      allow(Project).to receive(:find_with_namespace).and_return(project)
+      expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
 
       PostReceive.new.perform(pwd(project), key_id, base64_changes)
     end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3e4fee422409e3eea2cf079b409141db77189ca5
--- /dev/null
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe ProcessCommitWorker do
+  let(:worker) { described_class.new }
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:issue) { create(:issue, project: project, author: user) }
+  let(:commit) { project.commit }
+
+  describe '#perform' do
+    it 'does not process the commit when the project does not exist' do
+      expect(worker).not_to receive(:close_issues)
+
+      worker.perform(-1, user.id, commit.id)
+    end
+
+    it 'does not process the commit when the user does not exist' do
+      expect(worker).not_to receive(:close_issues)
+
+      worker.perform(project.id, -1, commit.id)
+    end
+
+    it 'does not process the commit when the commit no longer exists' do
+      expect(worker).not_to receive(:close_issues)
+
+      worker.perform(project.id, user.id, 'this-should-does-not-exist')
+    end
+
+    it 'processes the commit message' do
+      expect(worker).to receive(:process_commit_message).and_call_original
+
+      worker.perform(project.id, user.id, commit.id)
+    end
+
+    it 'updates the issue metrics' do
+      expect(worker).to receive(:update_issue_metrics).and_call_original
+
+      worker.perform(project.id, user.id, commit.id)
+    end
+  end
+
+  describe '#process_commit_message' do
+    context 'when pushing to the default branch' do
+      it 'closes issues that should be closed per the commit message' do
+        allow(commit).to receive(:safe_message).
+          and_return("Closes #{issue.to_reference}")
+
+        expect(worker).to receive(:close_issues).
+          with(project, user, user, commit, [issue])
+
+        worker.process_commit_message(project, commit, user, user, true)
+      end
+    end
+
+    context 'when pushing to a non-default branch' do
+      it 'does not close any issues' do
+        allow(commit).to receive(:safe_message).
+          and_return("Closes #{issue.to_reference}")
+
+        expect(worker).not_to receive(:close_issues)
+
+        worker.process_commit_message(project, commit, user, user, false)
+      end
+    end
+
+    it 'creates cross references' do
+      expect(commit).to receive(:create_cross_references!)
+
+      worker.process_commit_message(project, commit, user, user)
+    end
+  end
+
+  describe '#close_issues' do
+    context 'when the user can update the issues' do
+      it 'closes the issues' do
+        worker.close_issues(project, user, user, commit, [issue])
+
+        issue.reload
+
+        expect(issue.closed?).to eq(true)
+      end
+    end
+
+    context 'when the user can not update the issues' do
+      it 'does not close the issues' do
+        other_user = create(:user)
+
+        worker.close_issues(project, other_user, other_user, commit, [issue])
+
+        issue.reload
+
+        expect(issue.closed?).to eq(false)
+      end
+    end
+  end
+
+  describe '#update_issue_metrics' do
+    it 'updates any existing issue metrics' do
+      allow(commit).to receive(:safe_message).
+        and_return("Closes #{issue.to_reference}")
+
+      worker.update_issue_metrics(commit, user)
+
+      metric = Issue::Metrics.first
+
+      expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
+    end
+  end
+end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 5785a6a06ff9932d5355c4ebaca372b5d9241af3..bfa8c0ff2c6b6eaed9bb37f6c42b3209ccdb45cb 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -5,22 +5,60 @@ describe ProjectCacheWorker do
 
   subject { described_class.new }
 
+  describe '.perform_async' do
+    it 'schedules the job when no lease exists' do
+      allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+        and_return(false)
+
+      expect_any_instance_of(described_class).to receive(:perform)
+
+      described_class.perform_async(project.id)
+    end
+
+    it 'does not schedule the job when a lease exists' do
+      allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+        and_return(true)
+
+      expect_any_instance_of(described_class).not_to receive(:perform)
+
+      described_class.perform_async(project.id)
+    end
+  end
+
   describe '#perform' do
-    it 'updates project cache data' do
-      expect_any_instance_of(Repository).to receive(:size)
-      expect_any_instance_of(Repository).to receive(:commit_count)
+    context 'when an exclusive lease can be obtained' do
+      before do
+        allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+          and_return(true)
+      end
+
+      it 'updates project cache data' do
+        expect_any_instance_of(Repository).to receive(:size)
+        expect_any_instance_of(Repository).to receive(:commit_count)
 
-      expect_any_instance_of(Project).to receive(:update_repository_size)
-      expect_any_instance_of(Project).to receive(:update_commit_count)
+        expect_any_instance_of(Project).to receive(:update_repository_size)
+        expect_any_instance_of(Project).to receive(:update_commit_count)
 
-      subject.perform(project.id)
+        subject.perform(project.id)
+      end
+
+      it 'handles missing repository data' do
+        expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
+        expect_any_instance_of(Repository).not_to receive(:size)
+
+        subject.perform(project.id)
+      end
     end
 
-    it 'handles missing repository data' do
-      expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
-      expect_any_instance_of(Repository).not_to receive(:size)
+    context 'when an exclusive lease can not be obtained' do
+      it 'does nothing' do
+        allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+          and_return(false)
+
+        expect(subject).not_to receive(:update_caches)
 
-      subject.perform(project.id)
+        subject.perform(project.id)
+      end
     end
   end
 end
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35e1518a35e6c7fbb5f99c90659d34f45001cad5
--- /dev/null
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PruneOldEventsWorker do
+  describe '#perform' do
+    let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
+    let!(:not_expired_event) { create(:event, author_id: 0,  created_at: 1.day.ago) }
+    let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+
+    it 'prunes events older than 12 months' do
+      expect { subject.perform }.to change { Event.count }.by(-1)
+      expect(Event.find_by(id: expired_event.id)).to be_nil
+    end
+
+    it 'leaves fresh events' do
+      subject.perform
+      expect(not_expired_event.reload).to be_present
+    end
+
+    it 'leaves events from exactly 12 months ago' do
+      subject.perform
+      expect(exactly_12_months_event).to be_present
+    end
+  end
+end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..689bc3d27b4c46e8166478cd99c971bf31cd88d8
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+  describe '#perform' do
+    let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+    let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+    let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+    it 'removes expired group links' do
+      expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+      expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+    end
+
+    it 'leaves group links that expire in the future' do
+      subject.perform
+      expect(project_group_link_expiring_in_future.reload).to be_present
+    end
+
+    it 'leaves group links that do not expire at all' do
+      subject.perform
+      expect(non_expiring_project_group_link.reload).to be_present
+    end
+  end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..402aa1e714e44557a7a1f44266a70eb9d95280c2
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+  let(:worker) { RemoveExpiredMembersWorker.new }
+
+  describe '#perform' do
+    context 'project members' do
+      let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+      let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+      let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+      it 'removes expired members' do
+        expect { worker.perform }.to change { Member.count }.by(-1)
+        expect(Member.find_by(id: expired_project_member.id)).to be_nil
+      end
+
+      it 'leaves members that expire in the future' do
+        worker.perform
+        expect(project_member_expiring_in_future.reload).to be_present
+      end
+
+      it 'leaves members that do not expire at all' do
+        worker.perform
+        expect(non_expiring_project_member.reload).to be_present
+      end
+    end
+
+    context 'group members' do
+      let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+      let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+      let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+      it 'removes expired members' do
+        expect { worker.perform }.to change { Member.count }.by(-1)
+        expect(Member.find_by(id: expired_group_member.id)).to be_nil
+      end
+
+      it 'leaves members that expire in the future' do
+        worker.perform
+        expect(group_member_expiring_in_future.reload).to be_present
+      end
+
+      it 'leaves members that do not expire at all' do
+        worker.perform
+        expect(non_expiring_group_member.reload).to be_present
+      end
+    end
+
+    context 'when the last group owner expires' do
+      let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+      it 'does not delete the owner' do
+        worker.perform
+        expect(expired_group_owner.reload).to be_present
+      end
+    end
+  end
+end
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d42946de3875599d0e95431570cb472cd294a24
--- /dev/null
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe RemoveUnreferencedLfsObjectsWorker do
+  let(:worker) { RemoveUnreferencedLfsObjectsWorker.new }
+
+  describe '#perform' do
+    let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
+    let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
+    let!(:project1) { create(:empty_project, lfs_enabled: true) }
+    let!(:project2) { create(:empty_project, lfs_enabled: true) }
+    let!(:referenced_lfs_object1) { create(:lfs_object, oid: '3') }
+    let!(:referenced_lfs_object2) { create(:lfs_object, oid: '4') }
+    let!(:lfs_objects_project1_1) do
+      create(:lfs_objects_project,
+                project: project1,
+                lfs_object: referenced_lfs_object1
+            )
+    end
+    let!(:lfs_objects_project2_1) do
+      create(:lfs_objects_project,
+                project: project2,
+                lfs_object: referenced_lfs_object1
+            )
+    end
+    let!(:lfs_objects_project1_2) do
+      create(:lfs_objects_project,
+                project: project1,
+                lfs_object: referenced_lfs_object2
+            )
+    end
+
+    it 'removes unreferenced lfs objects' do
+      worker.perform
+
+      expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
+      expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
+    end
+
+    it 'leaves referenced lfs objects' do
+      worker.perform
+
+      expect(referenced_lfs_object1.reload).to be_present
+      expect(referenced_lfs_object2.reload).to be_present
+    end
+
+    it 'removes unreferenced lfs objects after project removal' do
+      project1.destroy
+
+      worker.perform
+
+      expect(referenced_lfs_object1.reload).to be_present
+      expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
+    end
+  end
+end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 05e07789dac62c4361b3b9f7fe03cf013dc8bb54..59cfb2c8e3a92edac50e474116ee10877cdb472b 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
   subject { described_class.new }
 
   it 'passes when the project has no push events' do
-    project = create(:project_empty_repo, wiki_enabled: false)
+    project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
     project.events.destroy_all
     break_repo(project)
 
@@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
   end
 
   it 'fails if the wiki repository is broken' do
-    project = create(:project_empty_repo, wiki_enabled: true)
+    project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
     project.create_wiki
 
     # Test sanity: everything should be fine before the wiki repo is broken
@@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
   end
 
   it 'skips wikis when disabled' do
-    project = create(:project_empty_repo, wiki_enabled: false)
+    project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
     # Make sure the test would fail if the wiki repo was checked
     break_wiki(project)
 
@@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
   end
 
   it 'creates missing wikis' do
-    project = create(:project_empty_repo, wiki_enabled: true)
+    project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
     FileUtils.rm_rf(wiki_path(project))
 
     subject.perform(project.id)
diff --git a/spec/workers/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c3c6fdcf2d5ceb5fcbf113caaa7c6372554d3fd7
--- /dev/null
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe TrendingProjectsWorker do
+  describe '#perform' do
+    it 'refreshes the trending projects' do
+      expect(TrendingProject).to receive(:refresh!)
+
+      described_class.new.perform
+    end
+  end
+end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c78a69eda675a464dc71f9157da7855c54a19e47
--- /dev/null
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UpdateMergeRequestsWorker do
+  include RepoHelpers
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  subject { described_class.new }
+
+  describe '#perform' do
+    let(:oldrev) { "123456" }
+    let(:newrev) { "789012" }
+    let(:ref)    { "refs/heads/test" }
+
+    def perform
+      subject.perform(project.id, user.id, oldrev, newrev, ref)
+    end
+
+    it 'executes MergeRequests::RefreshService with expected values' do
+      expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original
+      expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref)
+
+      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/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js
old mode 100755
new mode 100644
diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9e57bcb855dc58b796620a477d0c8a03d2e4624
--- /dev/null
+++ b/vendor/assets/javascripts/Sortable.js
@@ -0,0 +1,1374 @@
+/**!
+ * Sortable
+ * @author	RubaXa   <trash@rubaxa.org>
+ * @license MIT
+ */
+
+(function sortableModule(factory) {
+	"use strict";
+
+	if (typeof define === "function" && define.amd) {
+		define(factory);
+	}
+	else if (typeof module != "undefined" && typeof module.exports != "undefined") {
+		module.exports = factory();
+	}
+	else if (typeof Package !== "undefined") {
+		//noinspection JSUnresolvedVariable
+		Sortable = factory();  // export for Meteor.js
+	}
+	else {
+		/* jshint sub:true */
+		window["Sortable"] = factory();
+	}
+})(function sortableFactory() {
+	"use strict";
+
+	if (typeof window == "undefined" || !window.document) {
+		return function sortableError() {
+			throw new Error("Sortable.js requires a window with a document");
+		};
+	}
+
+	var dragEl,
+		parentEl,
+		ghostEl,
+		cloneEl,
+		rootEl,
+		nextEl,
+
+		scrollEl,
+		scrollParentEl,
+		scrollCustomFn,
+
+		lastEl,
+		lastCSS,
+		lastParentCSS,
+
+		oldIndex,
+		newIndex,
+
+		activeGroup,
+		putSortable,
+
+		autoScroll = {},
+
+		tapEvt,
+		touchEvt,
+
+		moved,
+
+		/** @const */
+		RSPACE = /\s+/g,
+
+		expando = 'Sortable' + (new Date).getTime(),
+
+		win = window,
+		document = win.document,
+		parseInt = win.parseInt,
+
+		$ = win.jQuery || win.Zepto,
+		Polymer = win.Polymer,
+
+		supportDraggable = !!('draggable' in document.createElement('div')),
+		supportCssPointerEvents = (function (el) {
+			// false when IE11
+			if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) {
+				return false;
+			}
+			el = document.createElement('x');
+			el.style.cssText = 'pointer-events:auto';
+			return el.style.pointerEvents === 'auto';
+		})(),
+
+		_silent = false,
+
+		abs = Math.abs,
+		min = Math.min,
+		slice = [].slice,
+
+		touchDragOverListeners = [],
+
+		_autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
+			// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+			if (rootEl && options.scroll) {
+				var el,
+					rect,
+					sens = options.scrollSensitivity,
+					speed = options.scrollSpeed,
+
+					x = evt.clientX,
+					y = evt.clientY,
+
+					winWidth = window.innerWidth,
+					winHeight = window.innerHeight,
+
+					vx,
+					vy,
+
+					scrollOffsetX,
+					scrollOffsetY
+				;
+
+				// Delect scrollEl
+				if (scrollParentEl !== rootEl) {
+					scrollEl = options.scroll;
+					scrollParentEl = rootEl;
+					scrollCustomFn = options.scrollFn;
+
+					if (scrollEl === true) {
+						scrollEl = rootEl;
+
+						do {
+							if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
+								(scrollEl.offsetHeight < scrollEl.scrollHeight)
+							) {
+								break;
+							}
+							/* jshint boss:true */
+						} while (scrollEl = scrollEl.parentNode);
+					}
+				}
+
+				if (scrollEl) {
+					el = scrollEl;
+					rect = scrollEl.getBoundingClientRect();
+					vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
+					vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
+				}
+
+
+				if (!(vx || vy)) {
+					vx = (winWidth - x <= sens) - (x <= sens);
+					vy = (winHeight - y <= sens) - (y <= sens);
+
+					/* jshint expr:true */
+					(vx || vy) && (el = win);
+				}
+
+
+				if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
+					autoScroll.el = el;
+					autoScroll.vx = vx;
+					autoScroll.vy = vy;
+
+					clearInterval(autoScroll.pid);
+
+					if (el) {
+						autoScroll.pid = setInterval(function () {
+							scrollOffsetY = vy ? vy * speed : 0;
+							scrollOffsetX = vx ? vx * speed : 0;
+
+							if ('function' === typeof(scrollCustomFn)) {
+								return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
+							}
+
+							if (el === win) {
+								win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
+							} else {
+								el.scrollTop += scrollOffsetY;
+								el.scrollLeft += scrollOffsetX;
+							}
+						}, 24);
+					}
+				}
+			}
+		}, 30),
+
+		_prepareGroup = function (options) {
+			function toFn(value, pull) {
+				if (value === void 0 || value === true) {
+					value = group.name;
+				}
+
+				if (typeof value === 'function') {
+					return value;
+				} else {
+					return function (to, from) {
+						var fromGroup = from.options.group.name;
+
+						return pull
+							? value
+							: value && (value.join
+								? value.indexOf(fromGroup) > -1
+								: (fromGroup == value)
+							);
+					};
+				}
+			}
+
+			var group = {};
+			var originalGroup = options.group;
+
+			if (!originalGroup || typeof originalGroup != 'object') {
+				originalGroup = {name: originalGroup};
+			}
+
+			group.name = originalGroup.name;
+			group.checkPull = toFn(originalGroup.pull, true);
+			group.checkPut = toFn(originalGroup.put);
+
+			options.group = group;
+		}
+	;
+
+
+
+	/**
+	 * @class  Sortable
+	 * @param  {HTMLElement}  el
+	 * @param  {Object}       [options]
+	 */
+	function Sortable(el, options) {
+		if (!(el && el.nodeType && el.nodeType === 1)) {
+			throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
+		}
+
+		this.el = el; // root element
+		this.options = options = _extend({}, options);
+
+
+		// Export instance
+		el[expando] = this;
+
+
+		// Default options
+		var defaults = {
+			group: Math.random(),
+			sort: true,
+			disabled: false,
+			store: null,
+			handle: null,
+			scroll: true,
+			scrollSensitivity: 30,
+			scrollSpeed: 10,
+			draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+			ignore: 'a, img',
+			filter: null,
+			animation: 0,
+			setData: function (dataTransfer, dragEl) {
+				dataTransfer.setData('Text', dragEl.textContent);
+			},
+			dropBubble: false,
+			dragoverBubble: false,
+			dataIdAttr: 'data-id',
+			delay: 0,
+			forceFallback: false,
+			fallbackClass: 'sortable-fallback',
+			fallbackOnBody: false,
+			fallbackTolerance: 0,
+			fallbackOffset: {x: 0, y: 0}
+		};
+
+
+		// Set default options
+		for (var name in defaults) {
+			!(name in options) && (options[name] = defaults[name]);
+		}
+
+		_prepareGroup(options);
+
+		// Bind all private methods
+		for (var fn in this) {
+			if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+				this[fn] = this[fn].bind(this);
+			}
+		}
+
+		// Setup drag mode
+		this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+		// Bind events
+		_on(el, 'mousedown', this._onTapStart);
+		_on(el, 'touchstart', this._onTapStart);
+
+		if (this.nativeDraggable) {
+			_on(el, 'dragover', this);
+			_on(el, 'dragenter', this);
+		}
+
+		touchDragOverListeners.push(this._onDragOver);
+
+		// Restore sorting
+		options.store && this.sort(options.store.get(this));
+	}
+
+
+	Sortable.prototype = /** @lends Sortable.prototype */ {
+		constructor: Sortable,
+
+		_onTapStart: function (/** Event|TouchEvent */evt) {
+			var _this = this,
+				el = this.el,
+				options = this.options,
+				type = evt.type,
+				touch = evt.touches && evt.touches[0],
+				target = (touch || evt).target,
+				originalTarget = evt.target.shadowRoot && evt.path[0] || target,
+				filter = options.filter,
+				startIndex;
+
+			// Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+			if (dragEl) {
+				return;
+			}
+
+			if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
+				return; // only left button or enabled
+			}
+
+			if (options.handle && !_closest(originalTarget, options.handle, el)) {
+				return;
+			}
+
+			target = _closest(target, options.draggable, el);
+
+			if (!target) {
+				return;
+			}
+
+			// Get the index of the dragged element within its parent
+			startIndex = _index(target, options.draggable);
+
+			// Check filter
+			if (typeof filter === 'function') {
+				if (filter.call(this, evt, target, this)) {
+					_dispatchEvent(_this, originalTarget, 'filter', target, el, startIndex);
+					evt.preventDefault();
+					return; // cancel dnd
+				}
+			}
+			else if (filter) {
+				filter = filter.split(',').some(function (criteria) {
+					criteria = _closest(originalTarget, criteria.trim(), el);
+
+					if (criteria) {
+						_dispatchEvent(_this, criteria, 'filter', target, el, startIndex);
+						return true;
+					}
+				});
+
+				if (filter) {
+					evt.preventDefault();
+					return; // cancel dnd
+				}
+			}
+
+			// Prepare `dragstart`
+			this._prepareDragStart(evt, touch, target, startIndex);
+		},
+
+		_prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
+			var _this = this,
+				el = _this.el,
+				options = _this.options,
+				ownerDocument = el.ownerDocument,
+				dragStartFn;
+
+			if (target && !dragEl && (target.parentNode === el)) {
+				tapEvt = evt;
+
+				rootEl = el;
+				dragEl = target;
+				parentEl = dragEl.parentNode;
+				nextEl = dragEl.nextSibling;
+				activeGroup = options.group;
+				oldIndex = startIndex;
+
+				this._lastX = (touch || evt).clientX;
+				this._lastY = (touch || evt).clientY;
+
+				dragEl.style['will-change'] = 'transform';
+
+				dragStartFn = function () {
+					// Delayed drag has been triggered
+					// we can re-enable the events: touchmove/mousemove
+					_this._disableDelayedDrag();
+
+					// Make the element draggable
+					dragEl.draggable = _this.nativeDraggable;
+
+					// Chosen item
+					_toggleClass(dragEl, options.chosenClass, true);
+
+					// Bind the events: dragstart/dragend
+					_this._triggerDragStart(touch);
+
+					// Drag start event
+					_dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, oldIndex);
+				};
+
+				// Disable "draggable"
+				options.ignore.split(',').forEach(function (criteria) {
+					_find(dragEl, criteria.trim(), _disableDraggable);
+				});
+
+				_on(ownerDocument, 'mouseup', _this._onDrop);
+				_on(ownerDocument, 'touchend', _this._onDrop);
+				_on(ownerDocument, 'touchcancel', _this._onDrop);
+
+				if (options.delay) {
+					// If the user moves the pointer or let go the click or touch
+					// before the delay has been reached:
+					// disable the delayed drag
+					_on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+					_on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+					_on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+					_on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
+					_on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
+
+					_this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+				} else {
+					dragStartFn();
+				}
+			}
+		},
+
+		_disableDelayedDrag: function () {
+			var ownerDocument = this.el.ownerDocument;
+
+			clearTimeout(this._dragStartTimer);
+			_off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+			_off(ownerDocument, 'touchend', this._disableDelayedDrag);
+			_off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+			_off(ownerDocument, 'mousemove', this._disableDelayedDrag);
+			_off(ownerDocument, 'touchmove', this._disableDelayedDrag);
+		},
+
+		_triggerDragStart: function (/** Touch */touch) {
+			if (touch) {
+				// Touch device support
+				tapEvt = {
+					target: dragEl,
+					clientX: touch.clientX,
+					clientY: touch.clientY
+				};
+
+				this._onDragStart(tapEvt, 'touch');
+			}
+			else if (!this.nativeDraggable) {
+				this._onDragStart(tapEvt, true);
+			}
+			else {
+				_on(dragEl, 'dragend', this);
+				_on(rootEl, 'dragstart', this._onDragStart);
+			}
+
+			try {
+				if (document.selection) {
+					// Timeout neccessary for IE9
+					setTimeout(function () {
+						document.selection.empty();
+					});
+				} else {
+					window.getSelection().removeAllRanges();
+				}
+			} catch (err) {
+			}
+		},
+
+		_dragStarted: function () {
+			if (rootEl && dragEl) {
+				var options = this.options;
+
+				// Apply effect
+				_toggleClass(dragEl, options.ghostClass, true);
+				_toggleClass(dragEl, options.dragClass, false);
+
+				Sortable.active = this;
+
+				// Drag start event
+				_dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
+			}
+		},
+
+		_emulateDragOver: function () {
+			if (touchEvt) {
+				if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
+					return;
+				}
+
+				this._lastX = touchEvt.clientX;
+				this._lastY = touchEvt.clientY;
+
+				if (!supportCssPointerEvents) {
+					_css(ghostEl, 'display', 'none');
+				}
+
+				var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
+					parent = target,
+					i = touchDragOverListeners.length;
+
+				if (parent) {
+					do {
+						if (parent[expando]) {
+							while (i--) {
+								touchDragOverListeners[i]({
+									clientX: touchEvt.clientX,
+									clientY: touchEvt.clientY,
+									target: target,
+									rootEl: parent
+								});
+							}
+
+							break;
+						}
+
+						target = parent; // store last element
+					}
+					/* jshint boss:true */
+					while (parent = parent.parentNode);
+				}
+
+				if (!supportCssPointerEvents) {
+					_css(ghostEl, 'display', '');
+				}
+			}
+		},
+
+
+		_onTouchMove: function (/**TouchEvent*/evt) {
+			if (tapEvt) {
+				var	options = this.options,
+					fallbackTolerance = options.fallbackTolerance,
+					fallbackOffset = options.fallbackOffset,
+					touch = evt.touches ? evt.touches[0] : evt,
+					dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x,
+					dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y,
+					translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
+
+				// only set the status to dragging, when we are actually dragging
+				if (!Sortable.active) {
+					if (fallbackTolerance &&
+						min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
+					) {
+						return;
+					}
+
+					this._dragStarted();
+				}
+
+				// as well as creating the ghost element on the document body
+				this._appendGhost();
+
+				moved = true;
+				touchEvt = touch;
+
+				_css(ghostEl, 'webkitTransform', translate3d);
+				_css(ghostEl, 'mozTransform', translate3d);
+				_css(ghostEl, 'msTransform', translate3d);
+				_css(ghostEl, 'transform', translate3d);
+
+				evt.preventDefault();
+			}
+		},
+
+		_appendGhost: function () {
+			if (!ghostEl) {
+				var rect = dragEl.getBoundingClientRect(),
+					css = _css(dragEl),
+					options = this.options,
+					ghostRect;
+
+				ghostEl = dragEl.cloneNode(true);
+
+				_toggleClass(ghostEl, options.ghostClass, false);
+				_toggleClass(ghostEl, options.fallbackClass, true);
+				_toggleClass(ghostEl, options.dragClass, true);
+
+				_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
+				_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
+				_css(ghostEl, 'width', rect.width);
+				_css(ghostEl, 'height', rect.height);
+				_css(ghostEl, 'opacity', '0.8');
+				_css(ghostEl, 'position', 'fixed');
+				_css(ghostEl, 'zIndex', '100000');
+				_css(ghostEl, 'pointerEvents', 'none');
+
+				options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
+
+				// Fixing dimensions.
+				ghostRect = ghostEl.getBoundingClientRect();
+				_css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
+				_css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
+			}
+		},
+
+		_onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
+			var dataTransfer = evt.dataTransfer,
+				options = this.options;
+
+			this._offUpEvents();
+
+			if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') {
+				cloneEl = _clone(dragEl);
+				_css(cloneEl, 'display', 'none');
+				rootEl.insertBefore(cloneEl, dragEl);
+				_dispatchEvent(this, rootEl, 'clone', dragEl);
+			}
+
+			_toggleClass(dragEl, options.dragClass, true);
+
+			if (useFallback) {
+				if (useFallback === 'touch') {
+					// Bind touch events
+					_on(document, 'touchmove', this._onTouchMove);
+					_on(document, 'touchend', this._onDrop);
+					_on(document, 'touchcancel', this._onDrop);
+				} else {
+					// Old brwoser
+					_on(document, 'mousemove', this._onTouchMove);
+					_on(document, 'mouseup', this._onDrop);
+				}
+
+				this._loopId = setInterval(this._emulateDragOver, 50);
+			}
+			else {
+				if (dataTransfer) {
+					dataTransfer.effectAllowed = 'move';
+					options.setData && options.setData.call(this, dataTransfer, dragEl);
+				}
+
+				_on(document, 'drop', this);
+				setTimeout(this._dragStarted, 0);
+			}
+		},
+
+		_onDragOver: function (/**Event*/evt) {
+			var el = this.el,
+				target,
+				dragRect,
+				targetRect,
+				revert,
+				options = this.options,
+				group = options.group,
+				activeSortable = Sortable.active,
+				isOwner = (activeGroup === group),
+				canSort = options.sort;
+
+			if (evt.preventDefault !== void 0) {
+				evt.preventDefault();
+				!options.dragoverBubble && evt.stopPropagation();
+			}
+
+			moved = true;
+
+			if (activeGroup && !options.disabled &&
+				(isOwner
+					? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+					: (
+						putSortable === this ||
+						activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt)
+					)
+				) &&
+				(evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
+			) {
+				// Smart auto-scrolling
+				_autoScroll(evt, options, this.el);
+
+				if (_silent) {
+					return;
+				}
+
+				target = _closest(evt.target, options.draggable, el);
+				dragRect = dragEl.getBoundingClientRect();
+				putSortable = this;
+
+				if (revert) {
+					_cloneHide(true);
+					parentEl = rootEl; // actualization
+
+					if (cloneEl || nextEl) {
+						rootEl.insertBefore(dragEl, cloneEl || nextEl);
+					}
+					else if (!canSort) {
+						rootEl.appendChild(dragEl);
+					}
+
+					return;
+				}
+
+
+				if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
+					(el === evt.target) && (target = _ghostIsLast(el, evt))
+				) {
+					if (target) {
+						if (target.animated) {
+							return;
+						}
+
+						targetRect = target.getBoundingClientRect();
+					}
+
+					_cloneHide(isOwner);
+
+					if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) {
+						if (!dragEl.contains(el)) {
+							el.appendChild(dragEl);
+							parentEl = el; // actualization
+						}
+
+						this._animate(dragRect, dragEl);
+						target && this._animate(targetRect, target);
+					}
+				}
+				else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
+					if (lastEl !== target) {
+						lastEl = target;
+						lastCSS = _css(target);
+						lastParentCSS = _css(target.parentNode);
+					}
+
+					targetRect = target.getBoundingClientRect();
+
+					var width = targetRect.right - targetRect.left,
+						height = targetRect.bottom - targetRect.top,
+						floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
+							|| (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
+						isWide = (target.offsetWidth > dragEl.offsetWidth),
+						isLong = (target.offsetHeight > dragEl.offsetHeight),
+						halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
+						nextSibling = target.nextElementSibling,
+						moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt),
+						after
+					;
+
+					if (moveVector !== false) {
+						_silent = true;
+						setTimeout(_unsilent, 30);
+
+						_cloneHide(isOwner);
+
+						if (moveVector === 1 || moveVector === -1) {
+							after = (moveVector === 1);
+						}
+						else if (floating) {
+							var elTop = dragEl.offsetTop,
+								tgTop = target.offsetTop;
+
+							if (elTop === tgTop) {
+								after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
+							}
+							else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
+								after = (evt.clientY - targetRect.top) / height > 0.5;
+							} else {
+								after = tgTop > elTop;
+							}
+						} else {
+							after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
+						}
+
+						if (!dragEl.contains(el)) {
+							if (after && !nextSibling) {
+								el.appendChild(dragEl);
+							} else {
+								target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+							}
+						}
+
+						parentEl = dragEl.parentNode; // actualization
+
+						this._animate(dragRect, dragEl);
+						this._animate(targetRect, target);
+					}
+				}
+			}
+		},
+
+		_animate: function (prevRect, target) {
+			var ms = this.options.animation;
+
+			if (ms) {
+				var currentRect = target.getBoundingClientRect();
+
+				_css(target, 'transition', 'none');
+				_css(target, 'transform', 'translate3d('
+					+ (prevRect.left - currentRect.left) + 'px,'
+					+ (prevRect.top - currentRect.top) + 'px,0)'
+				);
+
+				target.offsetWidth; // repaint
+
+				_css(target, 'transition', 'all ' + ms + 'ms');
+				_css(target, 'transform', 'translate3d(0,0,0)');
+
+				clearTimeout(target.animated);
+				target.animated = setTimeout(function () {
+					_css(target, 'transition', '');
+					_css(target, 'transform', '');
+					target.animated = false;
+				}, ms);
+			}
+		},
+
+		_offUpEvents: function () {
+			var ownerDocument = this.el.ownerDocument;
+
+			_off(document, 'touchmove', this._onTouchMove);
+			_off(ownerDocument, 'mouseup', this._onDrop);
+			_off(ownerDocument, 'touchend', this._onDrop);
+			_off(ownerDocument, 'touchcancel', this._onDrop);
+		},
+
+		_onDrop: function (/**Event*/evt) {
+			var el = this.el,
+				options = this.options;
+
+			clearInterval(this._loopId);
+			clearInterval(autoScroll.pid);
+			clearTimeout(this._dragStartTimer);
+
+			// Unbind events
+			_off(document, 'mousemove', this._onTouchMove);
+
+			if (this.nativeDraggable) {
+				_off(document, 'drop', this);
+				_off(el, 'dragstart', this._onDragStart);
+			}
+
+			this._offUpEvents();
+
+			if (evt) {
+				if (moved) {
+					evt.preventDefault();
+					!options.dropBubble && evt.stopPropagation();
+				}
+
+				ghostEl && ghostEl.parentNode.removeChild(ghostEl);
+
+				if (dragEl) {
+					if (this.nativeDraggable) {
+						_off(dragEl, 'dragend', this);
+					}
+
+					_disableDraggable(dragEl);
+					dragEl.style['will-change'] = '';
+
+					// Remove class's
+					_toggleClass(dragEl, this.options.ghostClass, false);
+					_toggleClass(dragEl, this.options.chosenClass, false);
+
+					if (rootEl !== parentEl) {
+						newIndex = _index(dragEl, options.draggable);
+
+						if (newIndex >= 0) {
+
+							// Add event
+							_dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
+
+							// Remove event
+							_dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
+
+							// drag from one list and drop into another
+							_dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+							_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+						}
+					}
+					else {
+						// Remove clone
+						cloneEl && cloneEl.parentNode.removeChild(cloneEl);
+
+						if (dragEl.nextSibling !== nextEl) {
+							// Get the index of the dragged element within its parent
+							newIndex = _index(dragEl, options.draggable);
+
+							if (newIndex >= 0) {
+								// drag & drop within the same list
+								_dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
+								_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+							}
+						}
+					}
+
+					if (Sortable.active) {
+						/* jshint eqnull:true */
+						if (newIndex == null || newIndex === -1) {
+							newIndex = oldIndex;
+						}
+
+						_dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
+
+						// Save sorting
+						this.save();
+					}
+				}
+
+			}
+
+			this._nulling();
+		},
+
+		_nulling: function() {
+			rootEl =
+			dragEl =
+			parentEl =
+			ghostEl =
+			nextEl =
+			cloneEl =
+
+			scrollEl =
+			scrollParentEl =
+
+			tapEvt =
+			touchEvt =
+
+			moved =
+			newIndex =
+
+			lastEl =
+			lastCSS =
+
+			putSortable =
+			activeGroup =
+			Sortable.active = null;
+		},
+
+		handleEvent: function (/**Event*/evt) {
+			var type = evt.type;
+
+			if (type === 'dragover' || type === 'dragenter') {
+				if (dragEl) {
+					this._onDragOver(evt);
+					_globalDragOver(evt);
+				}
+			}
+			else if (type === 'drop' || type === 'dragend') {
+				this._onDrop(evt);
+			}
+		},
+
+
+		/**
+		 * Serializes the item into an array of string.
+		 * @returns {String[]}
+		 */
+		toArray: function () {
+			var order = [],
+				el,
+				children = this.el.children,
+				i = 0,
+				n = children.length,
+				options = this.options;
+
+			for (; i < n; i++) {
+				el = children[i];
+				if (_closest(el, options.draggable, this.el)) {
+					order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+				}
+			}
+
+			return order;
+		},
+
+
+		/**
+		 * Sorts the elements according to the array.
+		 * @param  {String[]}  order  order of the items
+		 */
+		sort: function (order) {
+			var items = {}, rootEl = this.el;
+
+			this.toArray().forEach(function (id, i) {
+				var el = rootEl.children[i];
+
+				if (_closest(el, this.options.draggable, rootEl)) {
+					items[id] = el;
+				}
+			}, this);
+
+			order.forEach(function (id) {
+				if (items[id]) {
+					rootEl.removeChild(items[id]);
+					rootEl.appendChild(items[id]);
+				}
+			});
+		},
+
+
+		/**
+		 * Save the current sorting
+		 */
+		save: function () {
+			var store = this.options.store;
+			store && store.set(this);
+		},
+
+
+		/**
+		 * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+		 * @param   {HTMLElement}  el
+		 * @param   {String}       [selector]  default: `options.draggable`
+		 * @returns {HTMLElement|null}
+		 */
+		closest: function (el, selector) {
+			return _closest(el, selector || this.options.draggable, this.el);
+		},
+
+
+		/**
+		 * Set/get option
+		 * @param   {string} name
+		 * @param   {*}      [value]
+		 * @returns {*}
+		 */
+		option: function (name, value) {
+			var options = this.options;
+
+			if (value === void 0) {
+				return options[name];
+			} else {
+				options[name] = value;
+
+				if (name === 'group') {
+					_prepareGroup(options);
+				}
+			}
+		},
+
+
+		/**
+		 * Destroy
+		 */
+		destroy: function () {
+			var el = this.el;
+
+			el[expando] = null;
+
+			_off(el, 'mousedown', this._onTapStart);
+			_off(el, 'touchstart', this._onTapStart);
+
+			if (this.nativeDraggable) {
+				_off(el, 'dragover', this);
+				_off(el, 'dragenter', this);
+			}
+
+			// Remove draggable attributes
+			Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+				el.removeAttribute('draggable');
+			});
+
+			touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
+
+			this._onDrop();
+
+			this.el = el = null;
+		}
+	};
+
+
+	function _cloneHide(state) {
+		if (cloneEl && (cloneEl.state !== state)) {
+			_css(cloneEl, 'display', state ? 'none' : '');
+			!state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
+			cloneEl.state = state;
+		}
+	}
+
+
+	function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
+		if (el) {
+			ctx = ctx || document;
+
+			do {
+				if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
+					return el;
+				}
+				/* jshint boss:true */
+			} while (el = _getParentOrHost(el));
+		}
+
+		return null;
+	}
+
+
+	function _getParentOrHost(el) {
+		var parent = el.host;
+
+		return (parent && parent.nodeType) ? parent : el.parentNode;
+	}
+
+
+	function _globalDragOver(/**Event*/evt) {
+		if (evt.dataTransfer) {
+			evt.dataTransfer.dropEffect = 'move';
+		}
+		evt.preventDefault();
+	}
+
+
+	function _on(el, event, fn) {
+		el.addEventListener(event, fn, false);
+	}
+
+
+	function _off(el, event, fn) {
+		el.removeEventListener(event, fn, false);
+	}
+
+
+	function _toggleClass(el, name, state) {
+		if (el) {
+			if (el.classList) {
+				el.classList[state ? 'add' : 'remove'](name);
+			}
+			else {
+				var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
+				el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
+			}
+		}
+	}
+
+
+	function _css(el, prop, val) {
+		var style = el && el.style;
+
+		if (style) {
+			if (val === void 0) {
+				if (document.defaultView && document.defaultView.getComputedStyle) {
+					val = document.defaultView.getComputedStyle(el, '');
+				}
+				else if (el.currentStyle) {
+					val = el.currentStyle;
+				}
+
+				return prop === void 0 ? val : val[prop];
+			}
+			else {
+				if (!(prop in style)) {
+					prop = '-webkit-' + prop;
+				}
+
+				style[prop] = val + (typeof val === 'string' ? '' : 'px');
+			}
+		}
+	}
+
+
+	function _find(ctx, tagName, iterator) {
+		if (ctx) {
+			var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
+
+			if (iterator) {
+				for (; i < n; i++) {
+					iterator(list[i], i);
+				}
+			}
+
+			return list;
+		}
+
+		return [];
+	}
+
+
+
+	function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
+		sortable = (sortable || rootEl[expando]);
+
+		var evt = document.createEvent('Event'),
+			options = sortable.options,
+			onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
+
+		evt.initEvent(name, true, true);
+
+		evt.to = rootEl;
+		evt.from = fromEl || rootEl;
+		evt.item = targetEl || rootEl;
+		evt.clone = cloneEl;
+
+		evt.oldIndex = startIndex;
+		evt.newIndex = newIndex;
+
+		rootEl.dispatchEvent(evt);
+
+		if (options[onName]) {
+			options[onName].call(sortable, evt);
+		}
+	}
+
+
+	function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) {
+		var evt,
+			sortable = fromEl[expando],
+			onMoveFn = sortable.options.onMove,
+			retVal;
+
+		evt = document.createEvent('Event');
+		evt.initEvent('move', true, true);
+
+		evt.to = toEl;
+		evt.from = fromEl;
+		evt.dragged = dragEl;
+		evt.draggedRect = dragRect;
+		evt.related = targetEl || toEl;
+		evt.relatedRect = targetRect || toEl.getBoundingClientRect();
+
+		fromEl.dispatchEvent(evt);
+
+		if (onMoveFn) {
+			retVal = onMoveFn.call(sortable, evt, originalEvt);
+		}
+
+		return retVal;
+	}
+
+
+	function _disableDraggable(el) {
+		el.draggable = false;
+	}
+
+
+	function _unsilent() {
+		_silent = false;
+	}
+
+
+	/** @returns {HTMLElement|false} */
+	function _ghostIsLast(el, evt) {
+		var lastEl = el.lastElementChild,
+			rect = lastEl.getBoundingClientRect();
+
+		// 5 — min delta
+		// abs — нельзя добавлять, а то глюки при наведении сверху
+		return (
+			(evt.clientY - (rect.top + rect.height) > 5) ||
+			(evt.clientX - (rect.right + rect.width) > 5)
+		) && lastEl;
+	}
+
+
+	/**
+	 * Generate id
+	 * @param   {HTMLElement} el
+	 * @returns {String}
+	 * @private
+	 */
+	function _generateId(el) {
+		var str = el.tagName + el.className + el.src + el.href + el.textContent,
+			i = str.length,
+			sum = 0;
+
+		while (i--) {
+			sum += str.charCodeAt(i);
+		}
+
+		return sum.toString(36);
+	}
+
+	/**
+	 * Returns the index of an element within its parent for a selected set of
+	 * elements
+	 * @param  {HTMLElement} el
+	 * @param  {selector} selector
+	 * @return {number}
+	 */
+	function _index(el, selector) {
+		var index = 0;
+
+		if (!el || !el.parentNode) {
+			return -1;
+		}
+
+		while (el && (el = el.previousElementSibling)) {
+			if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
+				index++;
+			}
+		}
+
+		return index;
+	}
+
+	function _matches(/**HTMLElement*/el, /**String*/selector) {
+		if (el) {
+			selector = selector.split('.');
+
+			var tag = selector.shift().toUpperCase(),
+				re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
+
+			return (
+				(tag === '' || el.nodeName.toUpperCase() == tag) &&
+				(!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
+			);
+		}
+
+		return false;
+	}
+
+	function _throttle(callback, ms) {
+		var args, _this;
+
+		return function () {
+			if (args === void 0) {
+				args = arguments;
+				_this = this;
+
+				setTimeout(function () {
+					if (args.length === 1) {
+						callback.call(_this, args[0]);
+					} else {
+						callback.apply(_this, args);
+					}
+
+					args = void 0;
+				}, ms);
+			}
+		};
+	}
+
+	function _extend(dst, src) {
+		if (dst && src) {
+			for (var key in src) {
+				if (src.hasOwnProperty(key)) {
+					dst[key] = src[key];
+				}
+			}
+		}
+
+		return dst;
+	}
+
+	function _clone(el) {
+		return $
+			? $(el).clone(true)[0]
+			: (Polymer && Polymer.dom
+				? Polymer.dom(el).cloneNode(true)
+				: el.cloneNode(true)
+			);
+	}
+
+
+	// Export utils
+	Sortable.utils = {
+		on: _on,
+		off: _off,
+		css: _css,
+		find: _find,
+		is: function (el, selector) {
+			return !!_closest(el, selector, el);
+		},
+		extend: _extend,
+		throttle: _throttle,
+		closest: _closest,
+		toggleClass: _toggleClass,
+		clone: _clone,
+		index: _index
+	};
+
+
+	/**
+	 * Create sortable instance
+	 * @param {HTMLElement}  el
+	 * @param {Object}      [options]
+	 */
+	Sortable.create = function (el, options) {
+		return new Sortable(el, options);
+	};
+
+
+	// Export
+	Sortable.version = '1.4.2';
+	return Sortable;
+});
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
old mode 100755
new mode 100644
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
index 1b1f4f0bd63e9f38ea635607ab0aa9971e5c650d..39d7d2306f8eca30429df7e7af6c9f2b54151037 100644
--- a/vendor/assets/javascripts/clipboard.js
+++ b/vendor/assets/javascripts/clipboard.js
@@ -154,12 +154,12 @@ function E () {
 E.prototype = {
 	on: function (name, callback, ctx) {
     var e = this.e || (this.e = {});
-    
+
     (e[name] || (e[name] = [])).push({
       fn: callback,
       ctx: ctx
     });
-    
+
     return this;
   },
 
@@ -169,7 +169,7 @@ E.prototype = {
       self.off(name, fn);
       callback.apply(ctx, arguments);
     };
-    
+
     return this.on(name, fn, ctx);
   },
 
@@ -178,11 +178,11 @@ E.prototype = {
     var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
     var i = 0;
     var len = evtArr.length;
-    
+
     for (i; i < len; i++) {
       evtArr[i].fn.apply(evtArr[i].ctx, data);
     }
-    
+
     return this;
   },
 
@@ -190,21 +190,21 @@ E.prototype = {
     var e = this.e || (this.e = {});
     var evts = e[name];
     var liveEvents = [];
-    
+
     if (evts && callback) {
       for (var i = 0, len = evts.length; i < len; i++) {
         if (evts[i].fn !== callback) liveEvents.push(evts[i]);
       }
     }
-    
+
     // Remove event from queue to prevent memory leak
     // Suggested by https://github.com/lazd
     // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
 
-    (liveEvents.length) 
+    (liveEvents.length)
       ? e[name] = liveEvents
       : delete e[name];
-    
+
     return this;
   }
 };
@@ -618,4 +618,4 @@ exports['default'] = Clipboard;
 module.exports = exports['default'];
 
 },{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
-});
\ No newline at end of file
+});
diff --git a/vendor/assets/javascripts/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js
deleted file mode 100644
index 6a3e394b403d5084b70b8ed7ccdc0d566f2fcbdb..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/jquery.cookie.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * jQuery Cookie plugin
- *
- * Copyright (c) 2010 Klaus Hartl (stilbuero.de)
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- */
-jQuery.cookie = function (key, value, options) {
-
-    // key and at least value given, set cookie...
-    if (arguments.length > 1 && String(value) !== "[object Object]") {
-        options = jQuery.extend({}, options);
-
-        if (value === null || value === undefined) {
-            options.expires = -1;
-        }
-
-        if (typeof options.expires === 'number') {
-            var days = options.expires, t = options.expires = new Date();
-            t.setDate(t.getDate() + days);
-        }
-
-        value = String(value);
-
-        return (document.cookie = [
-            encodeURIComponent(key), '=',
-            options.raw ? value : encodeURIComponent(value),
-            options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
-            options.path ? '; path=' + options.path : '',
-            options.domain ? '; domain=' + options.domain : '',
-            options.secure ? '; secure' : ''
-        ].join(''));
-    }
-
-    // key and possibly options given, get cookie...
-    options = value || {};
-    var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
-    return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
-};
diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
old mode 100755
new mode 100644
diff --git a/vendor/assets/javascripts/js.cookie.js b/vendor/assets/javascripts/js.cookie.js
new file mode 100644
index 0000000000000000000000000000000000000000..92dbba162c425aa7b836848dd25a3fbd4a06af2e
--- /dev/null
+++ b/vendor/assets/javascripts/js.cookie.js
@@ -0,0 +1,156 @@
+/*!
+ * JavaScript Cookie v2.1.3
+ * https://github.com/js-cookie/js-cookie
+ *
+ * Copyright 2006, 2015 Klaus Hartl & Fagner Brack
+ * Released under the MIT license
+ */
+;(function (factory) {
+	var registeredInModuleLoader = false;
+	if (typeof define === 'function' && define.amd) {
+		define(factory);
+		registeredInModuleLoader = true;
+	}
+	if (typeof exports === 'object') {
+		module.exports = factory();
+		registeredInModuleLoader = true;
+	}
+	if (!registeredInModuleLoader) {
+		var OldCookies = window.Cookies;
+		var api = window.Cookies = factory();
+		api.noConflict = function () {
+			window.Cookies = OldCookies;
+			return api;
+		};
+	}
+}(function () {
+	function extend () {
+		var i = 0;
+		var result = {};
+		for (; i < arguments.length; i++) {
+			var attributes = arguments[ i ];
+			for (var key in attributes) {
+				result[key] = attributes[key];
+			}
+		}
+		return result;
+	}
+
+	function init (converter) {
+		function api (key, value, attributes) {
+			var result;
+			if (typeof document === 'undefined') {
+				return;
+			}
+
+			// Write
+
+			if (arguments.length > 1) {
+				attributes = extend({
+					path: '/'
+				}, api.defaults, attributes);
+
+				if (typeof attributes.expires === 'number') {
+					var expires = new Date();
+					expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
+					attributes.expires = expires;
+				}
+
+				try {
+					result = JSON.stringify(value);
+					if (/^[\{\[]/.test(result)) {
+						value = result;
+					}
+				} catch (e) {}
+
+				if (!converter.write) {
+					value = encodeURIComponent(String(value))
+						.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
+				} else {
+					value = converter.write(value, key);
+				}
+
+				key = encodeURIComponent(String(key));
+				key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
+				key = key.replace(/[\(\)]/g, escape);
+
+				return (document.cookie = [
+					key, '=', value,
+					attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+					attributes.path ? '; path=' + attributes.path : '',
+					attributes.domain ? '; domain=' + attributes.domain : '',
+					attributes.secure ? '; secure' : ''
+				].join(''));
+			}
+
+			// Read
+
+			if (!key) {
+				result = {};
+			}
+
+			// To prevent the for loop in the first place assign an empty array
+			// in case there are no cookies at all. Also prevents odd result when
+			// calling "get()"
+			var cookies = document.cookie ? document.cookie.split('; ') : [];
+			var rdecode = /(%[0-9A-Z]{2})+/g;
+			var i = 0;
+
+			for (; i < cookies.length; i++) {
+				var parts = cookies[i].split('=');
+				var cookie = parts.slice(1).join('=');
+
+				if (cookie.charAt(0) === '"') {
+					cookie = cookie.slice(1, -1);
+				}
+
+				try {
+					var name = parts[0].replace(rdecode, decodeURIComponent);
+					cookie = converter.read ?
+						converter.read(cookie, name) : converter(cookie, name) ||
+						cookie.replace(rdecode, decodeURIComponent);
+
+					if (this.json) {
+						try {
+							cookie = JSON.parse(cookie);
+						} catch (e) {}
+					}
+
+					if (key === name) {
+						result = cookie;
+						break;
+					}
+
+					if (!key) {
+						result[name] = cookie;
+					}
+				} catch (e) {}
+			}
+
+			return result;
+		}
+
+		api.set = api;
+		api.get = function (key) {
+			return api.call(api, key);
+		};
+		api.getJSON = function () {
+			return api.apply({
+				json: true
+			}, [].slice.call(arguments));
+		};
+		api.defaults = {};
+
+		api.remove = function (key, attributes) {
+			api(key, '', extend(attributes, {
+				expires: -1
+			}));
+		};
+
+		api.withConverter = init;
+
+		return api;
+	}
+
+	return init(function () {});
+}));
\ No newline at end of file
diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js
index bc451506b6a71eab728b3480efdcac00ec2d9334..9fbfef03f6d61ac874c4aa8ad286e55f6d1fa0eb 100644
--- a/vendor/assets/javascripts/task_list.js
+++ b/vendor/assets/javascripts/task_list.js
@@ -1,15 +1,118 @@
-
+// The MIT License (MIT)
+//
+// Copyright (c) 2014 GitHub, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+// TaskList Behavior
+//
 /*= provides tasklist:enabled */
-
-
 /*= provides tasklist:disabled */
-
-
 /*= provides tasklist:change */
-
-
 /*= provides tasklist:changed */
-
+//
+//
+// Enables Task List update behavior.
+//
+// ### Example Markup
+//
+//   <div class="js-task-list-container">
+//     <ul class="task-list">
+//       <li class="task-list-item">
+//         <input type="checkbox" class="js-task-list-item-checkbox" disabled />
+//         text
+//       </li>
+//     </ul>
+//     <form>
+//       <textarea class="js-task-list-field">- [ ] text</textarea>
+//     </form>
+//   </div>
+//
+// ### Specification
+//
+// TaskLists MUST be contained in a `(div).js-task-list-container`.
+//
+// TaskList Items SHOULD be an a list (`UL`/`OL`) element.
+//
+// Task list items MUST match `(input).task-list-item-checkbox` and MUST be
+// `disabled` by default.
+//
+// TaskLists MUST have a `(textarea).js-task-list-field` form element whose
+// `value` attribute is the source (Markdown) to be udpated. The source MUST
+// follow the syntax guidelines.
+//
+// TaskList updates trigger `tasklist:change` events. If the change is
+// successful, `tasklist:changed` is fired. The change can be canceled.
+//
+// jQuery is required.
+//
+// ### Methods
+//
+// `.taskList('enable')` or `.taskList()`
+//
+// Enables TaskList updates for the container.
+//
+// `.taskList('disable')`
+//
+// Disables TaskList updates for the container.
+//
+//# ### Events
+//
+// `tasklist:enabled`
+//
+// Fired when the TaskList is enabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:disabled`
+//
+// Fired when the TaskList is disabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:change`
+//
+// Fired before the TaskList item change takes affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** Yes
+// * **Target** `.js-task-list-field`
+//
+// `tasklist:changed`
+//
+// Fired once the TaskList item change has taken affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-field`
+//
+// ### NOTE
+//
+// Task list checkboxes are rendered as disabled by default because rendered
+// user content is cached without regard for the viewer.
 (function() {
   var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem,
     indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
@@ -18,20 +121,48 @@
 
   complete = "[x]";
 
+  // Escapes the String for regular expression matching.
   escapePattern = function(str) {
     return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]");
   };
 
-  incompletePattern = RegExp("" + (escapePattern(incomplete)));
-
-  completePattern = RegExp("" + (escapePattern(complete)));
+  incompletePattern = RegExp("" + (escapePattern(incomplete))); // escape square brackets
+ // match all white space
+  completePattern = RegExp("" + (escapePattern(complete))); // match all cases
 
+  // Pattern used to identify all task list items.
+  // Useful when you need iterate over all items.
   itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))");
 
+  // prefix, consisting of
+  // optional leading whitespace
+  // zero or more blockquotes
+  // list item indicator
+  // optional whitespace prefix
+  // checkbox
+  // is followed by whitespace
+  // is not part of a [foo](url) link
+  // and is followed by zero or more links
+  // and either a non-link or the end of the string
+  // Used to filter out code fences from the source for comparison only.
+  // http://rubular.com/r/x5EwZVrloI
+  // Modified slightly due to issues with JS
   codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg;
 
+  // ```
+  // followed by optional language
+  // whitespace
+  // code
+  // whitespace
+  // ```
+  // Used to filter out potential mismatches (items not in lists).
+  // http://rubular.com/r/OInl6CiePy
   itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g");
 
+  // Given the source text, updates the appropriate task list item to match the
+  // given checked value.
+  //
+  // Returns the updated String text.
   updateTaskListItem = function(source, itemIndex, checked) {
     var clean, index, line, result;
     clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n");
@@ -55,6 +186,9 @@
     return result.join("\n");
   };
 
+  // Updates the $field value to reflect the state of $item.
+  // Triggers the `tasklist:change` event before the value has changed, and fires
+  // a `tasklist:changed` event once the value has changed.
   updateTaskList = function($item) {
     var $container, $field, checked, event, index;
     $container = $item.closest('.js-task-list-container');
@@ -70,10 +204,12 @@
     }
   };
 
+  // When the task list item checkbox is updated, submit the change
   $(document).on('change', '.task-list-item-checkbox', function() {
     return updateTaskList($(this));
   });
 
+  // Enables TaskList item changes.
   enableTaskList = function($container) {
     if ($container.find('.js-task-list-field').length > 0) {
       $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null);
@@ -81,6 +217,7 @@
     }
   };
 
+  // Enables a collection of TaskList containers.
   enableTaskLists = function($containers) {
     var container, i, len, results;
     results = [];
@@ -91,11 +228,13 @@
     return results;
   };
 
+  // Disable TaskList item changes.
   disableTaskList = function($container) {
     $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled');
     return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled');
   };
 
+  // Disables a collection of TaskList containers.
   disableTaskLists = function($containers) {
     var container, i, len, results;
     results = [];
diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js
new file mode 100644
index 0000000000000000000000000000000000000000..d7981dbec7e100b8799b278a343bf9ea8dfb798a
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.full.js
@@ -0,0 +1,1318 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global.VueResource = factory());
+}(this, function () { 'use strict';
+
+  /**
+   * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis)
+   */
+
+  var RESOLVED = 0;
+  var REJECTED = 1;
+  var PENDING = 2;
+
+  function Promise$2(executor) {
+
+      this.state = PENDING;
+      this.value = undefined;
+      this.deferred = [];
+
+      var promise = this;
+
+      try {
+          executor(function (x) {
+              promise.resolve(x);
+          }, function (r) {
+              promise.reject(r);
+          });
+      } catch (e) {
+          promise.reject(e);
+      }
+  }
+
+  Promise$2.reject = function (r) {
+      return new Promise$2(function (resolve, reject) {
+          reject(r);
+      });
+  };
+
+  Promise$2.resolve = function (x) {
+      return new Promise$2(function (resolve, reject) {
+          resolve(x);
+      });
+  };
+
+  Promise$2.all = function all(iterable) {
+      return new Promise$2(function (resolve, reject) {
+          var count = 0,
+              result = [];
+
+          if (iterable.length === 0) {
+              resolve(result);
+          }
+
+          function resolver(i) {
+              return function (x) {
+                  result[i] = x;
+                  count += 1;
+
+                  if (count === iterable.length) {
+                      resolve(result);
+                  }
+              };
+          }
+
+          for (var i = 0; i < iterable.length; i += 1) {
+              Promise$2.resolve(iterable[i]).then(resolver(i), reject);
+          }
+      });
+  };
+
+  Promise$2.race = function race(iterable) {
+      return new Promise$2(function (resolve, reject) {
+          for (var i = 0; i < iterable.length; i += 1) {
+              Promise$2.resolve(iterable[i]).then(resolve, reject);
+          }
+      });
+  };
+
+  var p$1 = Promise$2.prototype;
+
+  p$1.resolve = function resolve(x) {
+      var promise = this;
+
+      if (promise.state === PENDING) {
+          if (x === promise) {
+              throw new TypeError('Promise settled with itself.');
+          }
+
+          var called = false;
+
+          try {
+              var then = x && x['then'];
+
+              if (x !== null && typeof x === 'object' && typeof then === 'function') {
+                  then.call(x, function (x) {
+                      if (!called) {
+                          promise.resolve(x);
+                      }
+                      called = true;
+                  }, function (r) {
+                      if (!called) {
+                          promise.reject(r);
+                      }
+                      called = true;
+                  });
+                  return;
+              }
+          } catch (e) {
+              if (!called) {
+                  promise.reject(e);
+              }
+              return;
+          }
+
+          promise.state = RESOLVED;
+          promise.value = x;
+          promise.notify();
+      }
+  };
+
+  p$1.reject = function reject(reason) {
+      var promise = this;
+
+      if (promise.state === PENDING) {
+          if (reason === promise) {
+              throw new TypeError('Promise settled with itself.');
+          }
+
+          promise.state = REJECTED;
+          promise.value = reason;
+          promise.notify();
+      }
+  };
+
+  p$1.notify = function notify() {
+      var promise = this;
+
+      nextTick(function () {
+          if (promise.state !== PENDING) {
+              while (promise.deferred.length) {
+                  var deferred = promise.deferred.shift(),
+                      onResolved = deferred[0],
+                      onRejected = deferred[1],
+                      resolve = deferred[2],
+                      reject = deferred[3];
+
+                  try {
+                      if (promise.state === RESOLVED) {
+                          if (typeof onResolved === 'function') {
+                              resolve(onResolved.call(undefined, promise.value));
+                          } else {
+                              resolve(promise.value);
+                          }
+                      } else if (promise.state === REJECTED) {
+                          if (typeof onRejected === 'function') {
+                              resolve(onRejected.call(undefined, promise.value));
+                          } else {
+                              reject(promise.value);
+                          }
+                      }
+                  } catch (e) {
+                      reject(e);
+                  }
+              }
+          }
+      });
+  };
+
+  p$1.then = function then(onResolved, onRejected) {
+      var promise = this;
+
+      return new Promise$2(function (resolve, reject) {
+          promise.deferred.push([onResolved, onRejected, resolve, reject]);
+          promise.notify();
+      });
+  };
+
+  p$1.catch = function (onRejected) {
+      return this.then(undefined, onRejected);
+  };
+
+  var PromiseObj = window.Promise || Promise$2;
+
+  function Promise$1(executor, context) {
+
+      if (executor instanceof PromiseObj) {
+          this.promise = executor;
+      } else {
+          this.promise = new PromiseObj(executor.bind(context));
+      }
+
+      this.context = context;
+  }
+
+  Promise$1.all = function (iterable, context) {
+      return new Promise$1(PromiseObj.all(iterable), context);
+  };
+
+  Promise$1.resolve = function (value, context) {
+      return new Promise$1(PromiseObj.resolve(value), context);
+  };
+
+  Promise$1.reject = function (reason, context) {
+      return new Promise$1(PromiseObj.reject(reason), context);
+  };
+
+  Promise$1.race = function (iterable, context) {
+      return new Promise$1(PromiseObj.race(iterable), context);
+  };
+
+  var p = Promise$1.prototype;
+
+  p.bind = function (context) {
+      this.context = context;
+      return this;
+  };
+
+  p.then = function (fulfilled, rejected) {
+
+      if (fulfilled && fulfilled.bind && this.context) {
+          fulfilled = fulfilled.bind(this.context);
+      }
+
+      if (rejected && rejected.bind && this.context) {
+          rejected = rejected.bind(this.context);
+      }
+
+      return new Promise$1(this.promise.then(fulfilled, rejected), this.context);
+  };
+
+  p.catch = function (rejected) {
+
+      if (rejected && rejected.bind && this.context) {
+          rejected = rejected.bind(this.context);
+      }
+
+      return new Promise$1(this.promise.catch(rejected), this.context);
+  };
+
+  p.finally = function (callback) {
+
+      return this.then(function (value) {
+          callback.call(this);
+          return value;
+      }, function (reason) {
+          callback.call(this);
+          return PromiseObj.reject(reason);
+      });
+  };
+
+  var debug = false;
+  var util = {};
+  var array = [];
+  function Util (Vue) {
+      util = Vue.util;
+      debug = Vue.config.debug || !Vue.config.silent;
+  }
+
+  function warn(msg) {
+      if (typeof console !== 'undefined' && debug) {
+          console.warn('[VueResource warn]: ' + msg);
+      }
+  }
+
+  function error(msg) {
+      if (typeof console !== 'undefined') {
+          console.error(msg);
+      }
+  }
+
+  function nextTick(cb, ctx) {
+      return util.nextTick(cb, ctx);
+  }
+
+  function trim(str) {
+      return str.replace(/^\s*|\s*$/g, '');
+  }
+
+  var isArray = Array.isArray;
+
+  function isString(val) {
+      return typeof val === 'string';
+  }
+
+  function isBoolean(val) {
+      return val === true || val === false;
+  }
+
+  function isFunction(val) {
+      return typeof val === 'function';
+  }
+
+  function isObject(obj) {
+      return obj !== null && typeof obj === 'object';
+  }
+
+  function isPlainObject(obj) {
+      return isObject(obj) && Object.getPrototypeOf(obj) == Object.prototype;
+  }
+
+  function isFormData(obj) {
+      return typeof FormData !== 'undefined' && obj instanceof FormData;
+  }
+
+  function when(value, fulfilled, rejected) {
+
+      var promise = Promise$1.resolve(value);
+
+      if (arguments.length < 2) {
+          return promise;
+      }
+
+      return promise.then(fulfilled, rejected);
+  }
+
+  function options(fn, obj, opts) {
+
+      opts = opts || {};
+
+      if (isFunction(opts)) {
+          opts = opts.call(obj);
+      }
+
+      return merge(fn.bind({ $vm: obj, $options: opts }), fn, { $options: opts });
+  }
+
+  function each(obj, iterator) {
+
+      var i, key;
+
+      if (typeof obj.length == 'number') {
+          for (i = 0; i < obj.length; i++) {
+              iterator.call(obj[i], obj[i], i);
+          }
+      } else if (isObject(obj)) {
+          for (key in obj) {
+              if (obj.hasOwnProperty(key)) {
+                  iterator.call(obj[key], obj[key], key);
+              }
+          }
+      }
+
+      return obj;
+  }
+
+  var assign = Object.assign || _assign;
+
+  function merge(target) {
+
+      var args = array.slice.call(arguments, 1);
+
+      args.forEach(function (source) {
+          _merge(target, source, true);
+      });
+
+      return target;
+  }
+
+  function defaults(target) {
+
+      var args = array.slice.call(arguments, 1);
+
+      args.forEach(function (source) {
+
+          for (var key in source) {
+              if (target[key] === undefined) {
+                  target[key] = source[key];
+              }
+          }
+      });
+
+      return target;
+  }
+
+  function _assign(target) {
+
+      var args = array.slice.call(arguments, 1);
+
+      args.forEach(function (source) {
+          _merge(target, source);
+      });
+
+      return target;
+  }
+
+  function _merge(target, source, deep) {
+      for (var key in source) {
+          if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
+              if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
+                  target[key] = {};
+              }
+              if (isArray(source[key]) && !isArray(target[key])) {
+                  target[key] = [];
+              }
+              _merge(target[key], source[key], deep);
+          } else if (source[key] !== undefined) {
+              target[key] = source[key];
+          }
+      }
+  }
+
+  function root (options, next) {
+
+      var url = next(options);
+
+      if (isString(options.root) && !url.match(/^(https?:)?\//)) {
+          url = options.root + '/' + url;
+      }
+
+      return url;
+  }
+
+  function query (options, next) {
+
+      var urlParams = Object.keys(Url.options.params),
+          query = {},
+          url = next(options);
+
+      each(options.params, function (value, key) {
+          if (urlParams.indexOf(key) === -1) {
+              query[key] = value;
+          }
+      });
+
+      query = Url.params(query);
+
+      if (query) {
+          url += (url.indexOf('?') == -1 ? '?' : '&') + query;
+      }
+
+      return url;
+  }
+
+  /**
+   * URL Template v2.0.6 (https://github.com/bramstein/url-template)
+   */
+
+  function expand(url, params, variables) {
+
+      var tmpl = parse(url),
+          expanded = tmpl.expand(params);
+
+      if (variables) {
+          variables.push.apply(variables, tmpl.vars);
+      }
+
+      return expanded;
+  }
+
+  function parse(template) {
+
+      var operators = ['+', '#', '.', '/', ';', '?', '&'],
+          variables = [];
+
+      return {
+          vars: variables,
+          expand: function (context) {
+              return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
+                  if (expression) {
+
+                      var operator = null,
+                          values = [];
+
+                      if (operators.indexOf(expression.charAt(0)) !== -1) {
+                          operator = expression.charAt(0);
+                          expression = expression.substr(1);
+                      }
+
+                      expression.split(/,/g).forEach(function (variable) {
+                          var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
+                          values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
+                          variables.push(tmp[1]);
+                      });
+
+                      if (operator && operator !== '+') {
+
+                          var separator = ',';
+
+                          if (operator === '?') {
+                              separator = '&';
+                          } else if (operator !== '#') {
+                              separator = operator;
+                          }
+
+                          return (values.length !== 0 ? operator : '') + values.join(separator);
+                      } else {
+                          return values.join(',');
+                      }
+                  } else {
+                      return encodeReserved(literal);
+                  }
+              });
+          }
+      };
+  }
+
+  function getValues(context, operator, key, modifier) {
+
+      var value = context[key],
+          result = [];
+
+      if (isDefined(value) && value !== '') {
+          if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+              value = value.toString();
+
+              if (modifier && modifier !== '*') {
+                  value = value.substring(0, parseInt(modifier, 10));
+              }
+
+              result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+          } else {
+              if (modifier === '*') {
+                  if (Array.isArray(value)) {
+                      value.filter(isDefined).forEach(function (value) {
+                          result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+                      });
+                  } else {
+                      Object.keys(value).forEach(function (k) {
+                          if (isDefined(value[k])) {
+                              result.push(encodeValue(operator, value[k], k));
+                          }
+                      });
+                  }
+              } else {
+                  var tmp = [];
+
+                  if (Array.isArray(value)) {
+                      value.filter(isDefined).forEach(function (value) {
+                          tmp.push(encodeValue(operator, value));
+                      });
+                  } else {
+                      Object.keys(value).forEach(function (k) {
+                          if (isDefined(value[k])) {
+                              tmp.push(encodeURIComponent(k));
+                              tmp.push(encodeValue(operator, value[k].toString()));
+                          }
+                      });
+                  }
+
+                  if (isKeyOperator(operator)) {
+                      result.push(encodeURIComponent(key) + '=' + tmp.join(','));
+                  } else if (tmp.length !== 0) {
+                      result.push(tmp.join(','));
+                  }
+              }
+          }
+      } else {
+          if (operator === ';') {
+              result.push(encodeURIComponent(key));
+          } else if (value === '' && (operator === '&' || operator === '?')) {
+              result.push(encodeURIComponent(key) + '=');
+          } else if (value === '') {
+              result.push('');
+          }
+      }
+
+      return result;
+  }
+
+  function isDefined(value) {
+      return value !== undefined && value !== null;
+  }
+
+  function isKeyOperator(operator) {
+      return operator === ';' || operator === '&' || operator === '?';
+  }
+
+  function encodeValue(operator, value, key) {
+
+      value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeURIComponent(value);
+
+      if (key) {
+          return encodeURIComponent(key) + '=' + value;
+      } else {
+          return value;
+      }
+  }
+
+  function encodeReserved(str) {
+      return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) {
+          if (!/%[0-9A-Fa-f]/.test(part)) {
+              part = encodeURI(part);
+          }
+          return part;
+      }).join('');
+  }
+
+  function template (options) {
+
+      var variables = [],
+          url = expand(options.url, options.params, variables);
+
+      variables.forEach(function (key) {
+          delete options.params[key];
+      });
+
+      return url;
+  }
+
+  /**
+   * Service for URL templating.
+   */
+
+  var ie = document.documentMode;
+  var el = document.createElement('a');
+
+  function Url(url, params) {
+
+      var self = this || {},
+          options = url,
+          transform;
+
+      if (isString(url)) {
+          options = { url: url, params: params };
+      }
+
+      options = merge({}, Url.options, self.$options, options);
+
+      Url.transforms.forEach(function (handler) {
+          transform = factory(handler, transform, self.$vm);
+      });
+
+      return transform(options);
+  }
+
+  /**
+   * Url options.
+   */
+
+  Url.options = {
+      url: '',
+      root: null,
+      params: {}
+  };
+
+  /**
+   * Url transforms.
+   */
+
+  Url.transforms = [template, query, root];
+
+  /**
+   * Encodes a Url parameter string.
+   *
+   * @param {Object} obj
+   */
+
+  Url.params = function (obj) {
+
+      var params = [],
+          escape = encodeURIComponent;
+
+      params.add = function (key, value) {
+
+          if (isFunction(value)) {
+              value = value();
+          }
+
+          if (value === null) {
+              value = '';
+          }
+
+          this.push(escape(key) + '=' + escape(value));
+      };
+
+      serialize(params, obj);
+
+      return params.join('&').replace(/%20/g, '+');
+  };
+
+  /**
+   * Parse a URL and return its components.
+   *
+   * @param {String} url
+   */
+
+  Url.parse = function (url) {
+
+      if (ie) {
+          el.href = url;
+          url = el.href;
+      }
+
+      el.href = url;
+
+      return {
+          href: el.href,
+          protocol: el.protocol ? el.protocol.replace(/:$/, '') : '',
+          port: el.port,
+          host: el.host,
+          hostname: el.hostname,
+          pathname: el.pathname.charAt(0) === '/' ? el.pathname : '/' + el.pathname,
+          search: el.search ? el.search.replace(/^\?/, '') : '',
+          hash: el.hash ? el.hash.replace(/^#/, '') : ''
+      };
+  };
+
+  function factory(handler, next, vm) {
+      return function (options) {
+          return handler.call(vm, options, next);
+      };
+  }
+
+  function serialize(params, obj, scope) {
+
+      var array = isArray(obj),
+          plain = isPlainObject(obj),
+          hash;
+
+      each(obj, function (value, key) {
+
+          hash = isObject(value) || isArray(value);
+
+          if (scope) {
+              key = scope + '[' + (plain || hash ? key : '') + ']';
+          }
+
+          if (!scope && array) {
+              params.add(value.name, value.value);
+          } else if (hash) {
+              serialize(params, value, key);
+          } else {
+              params.add(key, value);
+          }
+      });
+  }
+
+  function xdrClient (request) {
+      return new Promise$1(function (resolve) {
+
+          var xdr = new XDomainRequest(),
+              handler = function (event) {
+
+              var response = request.respondWith(xdr.responseText, {
+                  status: xdr.status,
+                  statusText: xdr.statusText
+              });
+
+              resolve(response);
+          };
+
+          request.abort = function () {
+              return xdr.abort();
+          };
+
+          xdr.open(request.method, request.getUrl(), true);
+          xdr.timeout = 0;
+          xdr.onload = handler;
+          xdr.onerror = handler;
+          xdr.ontimeout = function () {};
+          xdr.onprogress = function () {};
+          xdr.send(request.getBody());
+      });
+  }
+
+  var ORIGIN_URL = Url.parse(location.href);
+  var SUPPORTS_CORS = 'withCredentials' in new XMLHttpRequest();
+
+  function cors (request, next) {
+
+      if (!isBoolean(request.crossOrigin) && crossOrigin(request)) {
+          request.crossOrigin = true;
+      }
+
+      if (request.crossOrigin) {
+
+          if (!SUPPORTS_CORS) {
+              request.client = xdrClient;
+          }
+
+          delete request.emulateHTTP;
+      }
+
+      next();
+  }
+
+  function crossOrigin(request) {
+
+      var requestUrl = Url.parse(Url(request));
+
+      return requestUrl.protocol !== ORIGIN_URL.protocol || requestUrl.host !== ORIGIN_URL.host;
+  }
+
+  function body (request, next) {
+
+      if (request.emulateJSON && isPlainObject(request.body)) {
+          request.body = Url.params(request.body);
+          request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+      }
+
+      if (isFormData(request.body)) {
+          delete request.headers['Content-Type'];
+      }
+
+      if (isPlainObject(request.body)) {
+          request.body = JSON.stringify(request.body);
+      }
+
+      next(function (response) {
+
+          var contentType = response.headers['Content-Type'];
+
+          if (isString(contentType) && contentType.indexOf('application/json') === 0) {
+
+              try {
+                  response.data = response.json();
+              } catch (e) {
+                  response.data = null;
+              }
+          } else {
+              response.data = response.text();
+          }
+      });
+  }
+
+  function jsonpClient (request) {
+      return new Promise$1(function (resolve) {
+
+          var name = request.jsonp || 'callback',
+              callback = '_jsonp' + Math.random().toString(36).substr(2),
+              body = null,
+              handler,
+              script;
+
+          handler = function (event) {
+
+              var status = 0;
+
+              if (event.type === 'load' && body !== null) {
+                  status = 200;
+              } else if (event.type === 'error') {
+                  status = 404;
+              }
+
+              resolve(request.respondWith(body, { status: status }));
+
+              delete window[callback];
+              document.body.removeChild(script);
+          };
+
+          request.params[name] = callback;
+
+          window[callback] = function (result) {
+              body = JSON.stringify(result);
+          };
+
+          script = document.createElement('script');
+          script.src = request.getUrl();
+          script.type = 'text/javascript';
+          script.async = true;
+          script.onload = handler;
+          script.onerror = handler;
+
+          document.body.appendChild(script);
+      });
+  }
+
+  function jsonp (request, next) {
+
+      if (request.method == 'JSONP') {
+          request.client = jsonpClient;
+      }
+
+      next(function (response) {
+
+          if (request.method == 'JSONP') {
+              response.data = response.json();
+          }
+      });
+  }
+
+  function before (request, next) {
+
+      if (isFunction(request.before)) {
+          request.before.call(this, request);
+      }
+
+      next();
+  }
+
+  /**
+   * HTTP method override Interceptor.
+   */
+
+  function method (request, next) {
+
+      if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) {
+          request.headers['X-HTTP-Method-Override'] = request.method;
+          request.method = 'POST';
+      }
+
+      next();
+  }
+
+  function header (request, next) {
+
+      request.method = request.method.toUpperCase();
+      request.headers = assign({}, Http.headers.common, !request.crossOrigin ? Http.headers.custom : {}, Http.headers[request.method.toLowerCase()], request.headers);
+
+      next();
+  }
+
+  /**
+   * Timeout Interceptor.
+   */
+
+  function timeout (request, next) {
+
+      var timeout;
+
+      if (request.timeout) {
+          timeout = setTimeout(function () {
+              request.abort();
+          }, request.timeout);
+      }
+
+      next(function (response) {
+
+          clearTimeout(timeout);
+      });
+  }
+
+  function xhrClient (request) {
+      return new Promise$1(function (resolve) {
+
+          var xhr = new XMLHttpRequest(),
+              handler = function (event) {
+
+              var response = request.respondWith('response' in xhr ? xhr.response : xhr.responseText, {
+                  status: xhr.status === 1223 ? 204 : xhr.status, // IE9 status bug
+                  statusText: xhr.status === 1223 ? 'No Content' : trim(xhr.statusText),
+                  headers: parseHeaders(xhr.getAllResponseHeaders())
+              });
+
+              resolve(response);
+          };
+
+          request.abort = function () {
+              return xhr.abort();
+          };
+
+          xhr.open(request.method, request.getUrl(), true);
+          xhr.timeout = 0;
+          xhr.onload = handler;
+          xhr.onerror = handler;
+
+          if (request.progress) {
+              if (request.method === 'GET') {
+                  xhr.addEventListener('progress', request.progress);
+              } else if (/^(POST|PUT)$/i.test(request.method)) {
+                  xhr.upload.addEventListener('progress', request.progress);
+              }
+          }
+
+          if (request.credentials === true) {
+              xhr.withCredentials = true;
+          }
+
+          each(request.headers || {}, function (value, header) {
+              xhr.setRequestHeader(header, value);
+          });
+
+          xhr.send(request.getBody());
+      });
+  }
+
+  function parseHeaders(str) {
+
+      var headers = {},
+          value,
+          name,
+          i;
+
+      each(trim(str).split('\n'), function (row) {
+
+          i = row.indexOf(':');
+          name = trim(row.slice(0, i));
+          value = trim(row.slice(i + 1));
+
+          if (headers[name]) {
+
+              if (isArray(headers[name])) {
+                  headers[name].push(value);
+              } else {
+                  headers[name] = [headers[name], value];
+              }
+          } else {
+
+              headers[name] = value;
+          }
+      });
+
+      return headers;
+  }
+
+  function Client (context) {
+
+      var reqHandlers = [sendRequest],
+          resHandlers = [],
+          handler;
+
+      if (!isObject(context)) {
+          context = null;
+      }
+
+      function Client(request) {
+          return new Promise$1(function (resolve) {
+
+              function exec() {
+
+                  handler = reqHandlers.pop();
+
+                  if (isFunction(handler)) {
+                      handler.call(context, request, next);
+                  } else {
+                      warn('Invalid interceptor of type ' + typeof handler + ', must be a function');
+                      next();
+                  }
+              }
+
+              function next(response) {
+
+                  if (isFunction(response)) {
+
+                      resHandlers.unshift(response);
+                  } else if (isObject(response)) {
+
+                      resHandlers.forEach(function (handler) {
+                          response = when(response, function (response) {
+                              return handler.call(context, response) || response;
+                          });
+                      });
+
+                      when(response, resolve);
+
+                      return;
+                  }
+
+                  exec();
+              }
+
+              exec();
+          }, context);
+      }
+
+      Client.use = function (handler) {
+          reqHandlers.push(handler);
+      };
+
+      return Client;
+  }
+
+  function sendRequest(request, resolve) {
+
+      var client = request.client || xhrClient;
+
+      resolve(client(request));
+  }
+
+  var classCallCheck = function (instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+      throw new TypeError("Cannot call a class as a function");
+    }
+  };
+
+  /**
+   * HTTP Response.
+   */
+
+  var Response = function () {
+      function Response(body, _ref) {
+          var url = _ref.url;
+          var headers = _ref.headers;
+          var status = _ref.status;
+          var statusText = _ref.statusText;
+          classCallCheck(this, Response);
+
+
+          this.url = url;
+          this.body = body;
+          this.headers = headers || {};
+          this.status = status || 0;
+          this.statusText = statusText || '';
+          this.ok = status >= 200 && status < 300;
+      }
+
+      Response.prototype.text = function text() {
+          return this.body;
+      };
+
+      Response.prototype.blob = function blob() {
+          return new Blob([this.body]);
+      };
+
+      Response.prototype.json = function json() {
+          return JSON.parse(this.body);
+      };
+
+      return Response;
+  }();
+
+  var Request = function () {
+      function Request(options) {
+          classCallCheck(this, Request);
+
+
+          this.method = 'GET';
+          this.body = null;
+          this.params = {};
+          this.headers = {};
+
+          assign(this, options);
+      }
+
+      Request.prototype.getUrl = function getUrl() {
+          return Url(this);
+      };
+
+      Request.prototype.getBody = function getBody() {
+          return this.body;
+      };
+
+      Request.prototype.respondWith = function respondWith(body, options) {
+          return new Response(body, assign(options || {}, { url: this.getUrl() }));
+      };
+
+      return Request;
+  }();
+
+  /**
+   * Service for sending network requests.
+   */
+
+  var CUSTOM_HEADERS = { 'X-Requested-With': 'XMLHttpRequest' };
+  var COMMON_HEADERS = { 'Accept': 'application/json, text/plain, */*' };
+  var JSON_CONTENT_TYPE = { 'Content-Type': 'application/json;charset=utf-8' };
+
+  function Http(options) {
+
+      var self = this || {},
+          client = Client(self.$vm);
+
+      defaults(options || {}, self.$options, Http.options);
+
+      Http.interceptors.forEach(function (handler) {
+          client.use(handler);
+      });
+
+      return client(new Request(options)).then(function (response) {
+
+          return response.ok ? response : Promise$1.reject(response);
+      }, function (response) {
+
+          if (response instanceof Error) {
+              error(response);
+          }
+
+          return Promise$1.reject(response);
+      });
+  }
+
+  Http.options = {};
+
+  Http.headers = {
+      put: JSON_CONTENT_TYPE,
+      post: JSON_CONTENT_TYPE,
+      patch: JSON_CONTENT_TYPE,
+      delete: JSON_CONTENT_TYPE,
+      custom: CUSTOM_HEADERS,
+      common: COMMON_HEADERS
+  };
+
+  Http.interceptors = [before, timeout, method, body, jsonp, header, cors];
+
+  ['get', 'delete', 'head', 'jsonp'].forEach(function (method) {
+
+      Http[method] = function (url, options) {
+          return this(assign(options || {}, { url: url, method: method }));
+      };
+  });
+
+  ['post', 'put', 'patch'].forEach(function (method) {
+
+      Http[method] = function (url, body, options) {
+          return this(assign(options || {}, { url: url, method: method, body: body }));
+      };
+  });
+
+  function Resource(url, params, actions, options) {
+
+      var self = this || {},
+          resource = {};
+
+      actions = assign({}, Resource.actions, actions);
+
+      each(actions, function (action, name) {
+
+          action = merge({ url: url, params: params || {} }, options, action);
+
+          resource[name] = function () {
+              return (self.$http || Http)(opts(action, arguments));
+          };
+      });
+
+      return resource;
+  }
+
+  function opts(action, args) {
+
+      var options = assign({}, action),
+          params = {},
+          body;
+
+      switch (args.length) {
+
+          case 2:
+
+              params = args[0];
+              body = args[1];
+
+              break;
+
+          case 1:
+
+              if (/^(POST|PUT|PATCH)$/i.test(options.method)) {
+                  body = args[0];
+              } else {
+                  params = args[0];
+              }
+
+              break;
+
+          case 0:
+
+              break;
+
+          default:
+
+              throw 'Expected up to 4 arguments [params, body], got ' + args.length + ' arguments';
+      }
+
+      options.body = body;
+      options.params = assign({}, options.params, params);
+
+      return options;
+  }
+
+  Resource.actions = {
+
+      get: { method: 'GET' },
+      save: { method: 'POST' },
+      query: { method: 'GET' },
+      update: { method: 'PUT' },
+      remove: { method: 'DELETE' },
+      delete: { method: 'DELETE' }
+
+  };
+
+  function plugin(Vue) {
+
+      if (plugin.installed) {
+          return;
+      }
+
+      Util(Vue);
+
+      Vue.url = Url;
+      Vue.http = Http;
+      Vue.resource = Resource;
+      Vue.Promise = Promise$1;
+
+      Object.defineProperties(Vue.prototype, {
+
+          $url: {
+              get: function () {
+                  return options(Vue.url, this, this.$options.url);
+              }
+          },
+
+          $http: {
+              get: function () {
+                  return options(Vue.http, this, this.$options.http);
+              }
+          },
+
+          $resource: {
+              get: function () {
+                  return Vue.resource.bind(this);
+              }
+          },
+
+          $promise: {
+              get: function () {
+                  var _this = this;
+
+                  return function (executor) {
+                      return new Vue.Promise(executor, _this);
+                  };
+              }
+          }
+
+      });
+  }
+
+  if (typeof window !== 'undefined' && window.Vue) {
+      window.Vue.use(plugin);
+  }
+
+  return plugin;
+
+}));
\ No newline at end of file
diff --git a/vendor/assets/javascripts/vue-resource.js.erb b/vendor/assets/javascripts/vue-resource.js.erb
new file mode 100644
index 0000000000000000000000000000000000000000..8001775ce981f7546f9cb372e52e9a5ebe18b07a
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue-resource.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue-resource.min.js b/vendor/assets/javascripts/vue-resource.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..6bff73a2a675ab5c5e346bcf151642052ee949fb
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.min.js
@@ -0,0 +1,7 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=Z,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(e){n.reject(e)}}function n(t,n){t instanceof nt?this.promise=t:this.promise=new nt(t.bind(n)),this.context=n}function e(t){rt=t.util,ot=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ot&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return rt.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return"string"==typeof t}function c(t){return t===!0||t===!1}function a(t){return"function"==typeof t}function f(t){return null!==t&&"object"==typeof t}function h(t){return f(t)&&Object.getPrototypeOf(t)==Object.prototype}function p(t){return"undefined"!=typeof FormData&&t instanceof FormData}function l(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function d(t,n,e){return e=e||{},a(e)&&(e=e.call(n)),v(t.bind({$vm:n,$options:e}),t,{$options:e})}function m(t,n){var e,o;if("number"==typeof t.length)for(e=0;e<t.length;e++)n.call(t[e],t[e],e);else if(f(t))for(o in t)t.hasOwnProperty(o)&&n.call(t[o],t[o],o);return t}function v(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n,!0)}),t}function y(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){for(var e in n)void 0===t[e]&&(t[e]=n[e])}),t}function b(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n)}),t}function g(t,n,e){for(var o in n)e&&(h(n[o])||ut(n[o]))?(h(n[o])&&!h(t[o])&&(t[o]={}),ut(n[o])&&!ut(t[o])&&(t[o]=[]),g(t[o],n[o],e)):void 0!==n[o]&&(t[o]=n[o])}function w(t,n){var e=n(t);return s(t.root)&&!e.match(/^(https?:)?\//)&&(e=t.root+"/"+e),e}function T(t,n){var e=Object.keys(R.options.params),o={},r=n(t);return m(t.params,function(t,n){e.indexOf(n)===-1&&(o[n]=t)}),o=R.params(o),o&&(r+=(r.indexOf("?")==-1?"?":"&")+o),r}function j(t,n,e){var o=E(t),r=o.expand(n);return e&&e.push.apply(e,o.vars),r}function E(t){var n=["+","#",".","/",";","?","&"],e=[];return{vars:e,expand:function(o){return t.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g,function(t,r,i){if(r){var u=null,s=[];if(n.indexOf(r.charAt(0))!==-1&&(u=r.charAt(0),r=r.substr(1)),r.split(/,/g).forEach(function(t){var n=/([^:\*]*)(?::(\d+)|(\*))?/.exec(t);s.push.apply(s,x(o,u,n[1],n[2]||n[3])),e.push(n[1])}),u&&"+"!==u){var c=",";return"?"===u?c="&":"#"!==u&&(c=u),(0!==s.length?u:"")+s.join(c)}return s.join(",")}return $(i)})}}}function x(t,n,e,o){var r=t[e],i=[];if(O(r)&&""!==r)if("string"==typeof r||"number"==typeof r||"boolean"==typeof r)r=r.toString(),o&&"*"!==o&&(r=r.substring(0,parseInt(o,10))),i.push(C(n,r,P(n)?e:null));else if("*"===o)Array.isArray(r)?r.filter(O).forEach(function(t){i.push(C(n,t,P(n)?e:null))}):Object.keys(r).forEach(function(t){O(r[t])&&i.push(C(n,r[t],t))});else{var u=[];Array.isArray(r)?r.filter(O).forEach(function(t){u.push(C(n,t))}):Object.keys(r).forEach(function(t){O(r[t])&&(u.push(encodeURIComponent(t)),u.push(C(n,r[t].toString())))}),P(n)?i.push(encodeURIComponent(e)+"="+u.join(",")):0!==u.length&&i.push(u.join(","))}else";"===n?i.push(encodeURIComponent(e)):""!==r||"&"!==n&&"?"!==n?""===r&&i.push(""):i.push(encodeURIComponent(e)+"=");return i}function O(t){return void 0!==t&&null!==t}function P(t){return";"===t||"&"===t||"?"===t}function C(t,n,e){return n="+"===t||"#"===t?$(n):encodeURIComponent(n),e?encodeURIComponent(e)+"="+n:n}function $(t){return t.split(/(%[0-9A-Fa-f]{2})/g).map(function(t){return/%[0-9A-Fa-f]/.test(t)||(t=encodeURI(t)),t}).join("")}function U(t){var n=[],e=j(t.url,t.params,n);return n.forEach(function(n){delete t.params[n]}),e}function R(t,n){var e,o=this||{},r=t;return s(t)&&(r={url:t,params:n}),r=v({},R.options,o.$options,r),R.transforms.forEach(function(t){e=A(t,e,o.$vm)}),e(r)}function A(t,n,e){return function(o){return t.call(e,o,n)}}function S(t,n,e){var o,r=ut(n),i=h(n);m(n,function(n,u){o=f(n)||ut(n),e&&(u=e+"["+(i||o?u:"")+"]"),!e&&r?t.add(n.name,n.value):o?S(t,n,u):t.add(u,n)})}function k(t){return new n(function(n){var e=new XDomainRequest,o=function(o){var r=t.respondWith(e.responseText,{status:e.status,statusText:e.statusText});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,e.ontimeout=function(){},e.onprogress=function(){},e.send(t.getBody())})}function H(t,n){!c(t.crossOrigin)&&I(t)&&(t.crossOrigin=!0),t.crossOrigin&&(ht||(t.client=k),delete t.emulateHTTP),n()}function I(t){var n=R.parse(R(t));return n.protocol!==ft.protocol||n.host!==ft.host}function L(t,n){t.emulateJSON&&h(t.body)&&(t.body=R.params(t.body),t.headers["Content-Type"]="application/x-www-form-urlencoded"),p(t.body)&&delete t.headers["Content-Type"],h(t.body)&&(t.body=JSON.stringify(t.body)),n(function(t){var n=t.headers["Content-Type"];if(s(n)&&0===n.indexOf("application/json"))try{t.data=t.json()}catch(e){t.data=null}else t.data=t.text()})}function q(t){return new n(function(n){var e,o,r=t.jsonp||"callback",i="_jsonp"+Math.random().toString(36).substr(2),u=null;e=function(e){var r=0;"load"===e.type&&null!==u?r=200:"error"===e.type&&(r=404),n(t.respondWith(u,{status:r})),delete window[i],document.body.removeChild(o)},t.params[r]=i,window[i]=function(t){u=JSON.stringify(t)},o=document.createElement("script"),o.src=t.getUrl(),o.type="text/javascript",o.async=!0,o.onload=e,o.onerror=e,document.body.appendChild(o)})}function N(t,n){"JSONP"==t.method&&(t.client=q),n(function(n){"JSONP"==t.method&&(n.data=n.json())})}function D(t,n){a(t.before)&&t.before.call(this,t),n()}function J(t,n){t.emulateHTTP&&/^(PUT|PATCH|DELETE)$/i.test(t.method)&&(t.headers["X-HTTP-Method-Override"]=t.method,t.method="POST"),n()}function M(t,n){t.method=t.method.toUpperCase(),t.headers=st({},V.headers.common,t.crossOrigin?{}:V.headers.custom,V.headers[t.method.toLowerCase()],t.headers),n()}function X(t,n){var e;t.timeout&&(e=setTimeout(function(){t.abort()},t.timeout)),n(function(t){clearTimeout(e)})}function W(t){return new n(function(n){var e=new XMLHttpRequest,o=function(o){var r=t.respondWith("response"in e?e.response:e.responseText,{status:1223===e.status?204:e.status,statusText:1223===e.status?"No Content":u(e.statusText),headers:B(e.getAllResponseHeaders())});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,t.progress&&("GET"===t.method?e.addEventListener("progress",t.progress):/^(POST|PUT)$/i.test(t.method)&&e.upload.addEventListener("progress",t.progress)),t.credentials===!0&&(e.withCredentials=!0),m(t.headers||{},function(t,n){e.setRequestHeader(n,t)}),e.send(t.getBody())})}function B(t){var n,e,o,r={};return m(u(t).split("\n"),function(t){o=t.indexOf(":"),e=u(t.slice(0,o)),n=u(t.slice(o+1)),r[e]?ut(r[e])?r[e].push(n):r[e]=[r[e],n]:r[e]=n}),r}function F(t){function e(e){return new n(function(n){function s(){r=i.pop(),a(r)?r.call(t,e,c):(o("Invalid interceptor of type "+typeof r+", must be a function"),c())}function c(e){if(a(e))u.unshift(e);else if(f(e))return u.forEach(function(n){e=l(e,function(e){return n.call(t,e)||e})}),void l(e,n);s()}s()},t)}var r,i=[G],u=[];return f(t)||(t=null),e.use=function(t){i.push(t)},e}function G(t,n){var e=t.client||W;n(e(t))}function V(t){var e=this||{},o=F(e.$vm);return y(t||{},e.$options,V.options),V.interceptors.forEach(function(t){o.use(t)}),o(new dt(t)).then(function(t){return t.ok?t:n.reject(t)},function(t){return t instanceof Error&&r(t),n.reject(t)})}function _(t,n,e,o){var r=this||{},i={};return e=st({},_.actions,e),m(e,function(e,u){e=v({url:t,params:n||{}},o,e),i[u]=function(){return(r.$http||V)(z(e,arguments))}}),i}function z(t,n){var e,o=st({},t),r={};switch(n.length){case 2:r=n[0],e=n[1];break;case 1:/^(POST|PUT|PATCH)$/i.test(o.method)?e=n[0]:r=n[0];break;case 0:break;default:throw"Expected up to 4 arguments [params, body], got "+n.length+" arguments"}return o.body=e,o.params=st({},o.params,r),o}function K(t){K.installed||(e(t),t.url=R,t.http=V,t.resource=_,t.Promise=n,Object.defineProperties(t.prototype,{$url:{get:function(){return d(t.url,this,this.$options.url)}},$http:{get:function(){return d(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}},$promise:{get:function(){var n=this;return function(e){return new t.Promise(e,n)}}}}))}var Q=0,Y=1,Z=2;t.reject=function(n){return new t(function(t,e){e(n)})},t.resolve=function(n){return new t(function(t,e){t(n)})},t.all=function(n){return new t(function(e,o){function r(t){return function(o){u[t]=o,i+=1,i===n.length&&e(u)}}var i=0,u=[];0===n.length&&e(u);for(var s=0;s<n.length;s+=1)t.resolve(n[s]).then(r(s),o)})},t.race=function(n){return new t(function(e,o){for(var r=0;r<n.length;r+=1)t.resolve(n[r]).then(e,o)})};var tt=t.prototype;tt.resolve=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");var e=!1;try{var o=t&&t.then;if(null!==t&&"object"==typeof t&&"function"==typeof o)return void o.call(t,function(t){e||n.resolve(t),e=!0},function(t){e||n.reject(t),e=!0})}catch(r){return void(e||n.reject(r))}n.state=Q,n.value=t,n.notify()}},tt.reject=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");n.state=Y,n.value=t,n.notify()}},tt.notify=function(){var t=this;i(function(){if(t.state!==Z)for(;t.deferred.length;){var n=t.deferred.shift(),e=n[0],o=n[1],r=n[2],i=n[3];try{t.state===Q?r("function"==typeof e?e.call(void 0,t.value):t.value):t.state===Y&&("function"==typeof o?r(o.call(void 0,t.value)):i(t.value))}catch(u){i(u)}}})},tt.then=function(n,e){var o=this;return new t(function(t,r){o.deferred.push([n,e,t,r]),o.notify()})},tt["catch"]=function(t){return this.then(void 0,t)};var nt=window.Promise||t;n.all=function(t,e){return new n(nt.all(t),e)},n.resolve=function(t,e){return new n(nt.resolve(t),e)},n.reject=function(t,e){return new n(nt.reject(t),e)},n.race=function(t,e){return new n(nt.race(t),e)};var et=n.prototype;et.bind=function(t){return this.context=t,this},et.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),new n(this.promise.then(t,e),this.context)},et["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),new n(this.promise["catch"](t),this.context)},et["finally"]=function(t){return this.then(function(n){return t.call(this),n},function(n){return t.call(this),nt.reject(n)})};var ot=!1,rt={},it=[],ut=Array.isArray,st=Object.assign||b,ct=document.documentMode,at=document.createElement("a");R.options={url:"",root:null,params:{}},R.transforms=[U,T,w],R.params=function(t){var n=[],e=encodeURIComponent;return n.add=function(t,n){a(n)&&(n=n()),null===n&&(n=""),this.push(e(t)+"="+e(n))},S(n,t),n.join("&").replace(/%20/g,"+")},R.parse=function(t){return ct&&(at.href=t,t=at.href),at.href=t,{href:at.href,protocol:at.protocol?at.protocol.replace(/:$/,""):"",port:at.port,host:at.host,hostname:at.hostname,pathname:"/"===at.pathname.charAt(0)?at.pathname:"/"+at.pathname,search:at.search?at.search.replace(/^\?/,""):"",hash:at.hash?at.hash.replace(/^#/,""):""}};var ft=R.parse(location.href),ht="withCredentials"in new XMLHttpRequest,pt=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},lt=function(){function t(n,e){var o=e.url,r=e.headers,i=e.status,u=e.statusText;pt(this,t),this.url=o,this.body=n,this.headers=r||{},this.status=i||0,this.statusText=u||"",this.ok=i>=200&&i<300}return t.prototype.text=function(){return this.body},t.prototype.blob=function(){return new Blob([this.body])},t.prototype.json=function(){return JSON.parse(this.body)},t}(),dt=function(){function t(n){pt(this,t),this.method="GET",this.body=null,this.params={},this.headers={},st(this,n)}return t.prototype.getUrl=function(){return R(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new lt(t,st(n||{},{url:this.getUrl()}))},t}(),mt={"X-Requested-With":"XMLHttpRequest"},vt={Accept:"application/json, text/plain, */*"},yt={"Content-Type":"application/json;charset=utf-8"};return V.options={},V.headers={put:yt,post:yt,patch:yt,"delete":yt,custom:mt,common:vt},V.interceptors=[D,X,J,L,N,M,H],["get","delete","head","jsonp"].forEach(function(t){V[t]=function(n,e){return this(st(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){V[t]=function(n,e,o){return this(st(o||{},{url:n,method:t,body:e}))}}),_.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},"delete":{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(K),K});
\ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ae95897a016afe6110f7f70e1c3ff9524cffed1
--- /dev/null
+++ b/vendor/assets/javascripts/vue.full.js
@@ -0,0 +1,10073 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 2016 Evan You
+ * Released under the MIT License.
+ */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global.Vue = factory());
+}(this, function () { 'use strict';
+
+  function set(obj, key, val) {
+    if (hasOwn(obj, key)) {
+      obj[key] = val;
+      return;
+    }
+    if (obj._isVue) {
+      set(obj._data, key, val);
+      return;
+    }
+    var ob = obj.__ob__;
+    if (!ob) {
+      obj[key] = val;
+      return;
+    }
+    ob.convert(key, val);
+    ob.dep.notify();
+    if (ob.vms) {
+      var i = ob.vms.length;
+      while (i--) {
+        var vm = ob.vms[i];
+        vm._proxy(key);
+        vm._digest();
+      }
+    }
+    return val;
+  }
+
+  /**
+   * Delete a property and trigger change if necessary.
+   *
+   * @param {Object} obj
+   * @param {String} key
+   */
+
+  function del(obj, key) {
+    if (!hasOwn(obj, key)) {
+      return;
+    }
+    delete obj[key];
+    var ob = obj.__ob__;
+    if (!ob) {
+      if (obj._isVue) {
+        delete obj._data[key];
+        obj._digest();
+      }
+      return;
+    }
+    ob.dep.notify();
+    if (ob.vms) {
+      var i = ob.vms.length;
+      while (i--) {
+        var vm = ob.vms[i];
+        vm._unproxy(key);
+        vm._digest();
+      }
+    }
+  }
+
+  var hasOwnProperty = Object.prototype.hasOwnProperty;
+  /**
+   * Check whether the object has the property.
+   *
+   * @param {Object} obj
+   * @param {String} key
+   * @return {Boolean}
+   */
+
+  function hasOwn(obj, key) {
+    return hasOwnProperty.call(obj, key);
+  }
+
+  /**
+   * Check if an expression is a literal value.
+   *
+   * @param {String} exp
+   * @return {Boolean}
+   */
+
+  var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
+
+  function isLiteral(exp) {
+    return literalValueRE.test(exp);
+  }
+
+  /**
+   * Check if a string starts with $ or _
+   *
+   * @param {String} str
+   * @return {Boolean}
+   */
+
+  function isReserved(str) {
+    var c = (str + '').charCodeAt(0);
+    return c === 0x24 || c === 0x5F;
+  }
+
+  /**
+   * Guard text output, make sure undefined outputs
+   * empty string
+   *
+   * @param {*} value
+   * @return {String}
+   */
+
+  function _toString(value) {
+    return value == null ? '' : value.toString();
+  }
+
+  /**
+   * Check and convert possible numeric strings to numbers
+   * before setting back to data
+   *
+   * @param {*} value
+   * @return {*|Number}
+   */
+
+  function toNumber(value) {
+    if (typeof value !== 'string') {
+      return value;
+    } else {
+      var parsed = Number(value);
+      return isNaN(parsed) ? value : parsed;
+    }
+  }
+
+  /**
+   * Convert string boolean literals into real booleans.
+   *
+   * @param {*} value
+   * @return {*|Boolean}
+   */
+
+  function toBoolean(value) {
+    return value === 'true' ? true : value === 'false' ? false : value;
+  }
+
+  /**
+   * Strip quotes from a string
+   *
+   * @param {String} str
+   * @return {String | false}
+   */
+
+  function stripQuotes(str) {
+    var a = str.charCodeAt(0);
+    var b = str.charCodeAt(str.length - 1);
+    return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str;
+  }
+
+  /**
+   * Camelize a hyphen-delmited string.
+   *
+   * @param {String} str
+   * @return {String}
+   */
+
+  var camelizeRE = /-(\w)/g;
+
+  function camelize(str) {
+    return str.replace(camelizeRE, toUpper);
+  }
+
+  function toUpper(_, c) {
+    return c ? c.toUpperCase() : '';
+  }
+
+  /**
+   * Hyphenate a camelCase string.
+   *
+   * @param {String} str
+   * @return {String}
+   */
+
+  var hyphenateRE = /([a-z\d])([A-Z])/g;
+
+  function hyphenate(str) {
+    return str.replace(hyphenateRE, '$1-$2').toLowerCase();
+  }
+
+  /**
+   * Converts hyphen/underscore/slash delimitered names into
+   * camelized classNames.
+   *
+   * e.g. my-component => MyComponent
+   *      some_else    => SomeElse
+   *      some/comp    => SomeComp
+   *
+   * @param {String} str
+   * @return {String}
+   */
+
+  var classifyRE = /(?:^|[-_\/])(\w)/g;
+
+  function classify(str) {
+    return str.replace(classifyRE, toUpper);
+  }
+
+  /**
+   * Simple bind, faster than native
+   *
+   * @param {Function} fn
+   * @param {Object} ctx
+   * @return {Function}
+   */
+
+  function bind(fn, ctx) {
+    return function (a) {
+      var l = arguments.length;
+      return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
+    };
+  }
+
+  /**
+   * Convert an Array-like object to a real Array.
+   *
+   * @param {Array-like} list
+   * @param {Number} [start] - start index
+   * @return {Array}
+   */
+
+  function toArray(list, start) {
+    start = start || 0;
+    var i = list.length - start;
+    var ret = new Array(i);
+    while (i--) {
+      ret[i] = list[i + start];
+    }
+    return ret;
+  }
+
+  /**
+   * Mix properties into target object.
+   *
+   * @param {Object} to
+   * @param {Object} from
+   */
+
+  function extend(to, from) {
+    var keys = Object.keys(from);
+    var i = keys.length;
+    while (i--) {
+      to[keys[i]] = from[keys[i]];
+    }
+    return to;
+  }
+
+  /**
+   * Quick object check - this is primarily used to tell
+   * Objects from primitive values when we know the value
+   * is a JSON-compliant type.
+   *
+   * @param {*} obj
+   * @return {Boolean}
+   */
+
+  function isObject(obj) {
+    return obj !== null && typeof obj === 'object';
+  }
+
+  /**
+   * Strict object type check. Only returns true
+   * for plain JavaScript objects.
+   *
+   * @param {*} obj
+   * @return {Boolean}
+   */
+
+  var toString = Object.prototype.toString;
+  var OBJECT_STRING = '[object Object]';
+
+  function isPlainObject(obj) {
+    return toString.call(obj) === OBJECT_STRING;
+  }
+
+  /**
+   * Array type check.
+   *
+   * @param {*} obj
+   * @return {Boolean}
+   */
+
+  var isArray = Array.isArray;
+
+  /**
+   * Define a property.
+   *
+   * @param {Object} obj
+   * @param {String} key
+   * @param {*} val
+   * @param {Boolean} [enumerable]
+   */
+
+  function def(obj, key, val, enumerable) {
+    Object.defineProperty(obj, key, {
+      value: val,
+      enumerable: !!enumerable,
+      writable: true,
+      configurable: true
+    });
+  }
+
+  /**
+   * Debounce a function so it only gets called after the
+   * input stops arriving after the given wait period.
+   *
+   * @param {Function} func
+   * @param {Number} wait
+   * @return {Function} - the debounced function
+   */
+
+  function _debounce(func, wait) {
+    var timeout, args, context, timestamp, result;
+    var later = function later() {
+      var last = Date.now() - timestamp;
+      if (last < wait && last >= 0) {
+        timeout = setTimeout(later, wait - last);
+      } else {
+        timeout = null;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      }
+    };
+    return function () {
+      context = this;
+      args = arguments;
+      timestamp = Date.now();
+      if (!timeout) {
+        timeout = setTimeout(later, wait);
+      }
+      return result;
+    };
+  }
+
+  /**
+   * Manual indexOf because it's slightly faster than
+   * native.
+   *
+   * @param {Array} arr
+   * @param {*} obj
+   */
+
+  function indexOf(arr, obj) {
+    var i = arr.length;
+    while (i--) {
+      if (arr[i] === obj) return i;
+    }
+    return -1;
+  }
+
+  /**
+   * Make a cancellable version of an async callback.
+   *
+   * @param {Function} fn
+   * @return {Function}
+   */
+
+  function cancellable(fn) {
+    var cb = function cb() {
+      if (!cb.cancelled) {
+        return fn.apply(this, arguments);
+      }
+    };
+    cb.cancel = function () {
+      cb.cancelled = true;
+    };
+    return cb;
+  }
+
+  /**
+   * Check if two values are loosely equal - that is,
+   * if they are plain objects, do they have the same shape?
+   *
+   * @param {*} a
+   * @param {*} b
+   * @return {Boolean}
+   */
+
+  function looseEqual(a, b) {
+    /* eslint-disable eqeqeq */
+    return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false);
+    /* eslint-enable eqeqeq */
+  }
+
+  var hasProto = ('__proto__' in {});
+
+  // Browser environment sniffing
+  var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]';
+
+  // detect devtools
+  var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
+
+  // UA sniffing for working around browser-specific quirks
+  var UA = inBrowser && window.navigator.userAgent.toLowerCase();
+  var isIE = UA && UA.indexOf('trident') > 0;
+  var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
+  var isAndroid = UA && UA.indexOf('android') > 0;
+  var isIos = UA && /(iphone|ipad|ipod|ios)/i.test(UA);
+  var iosVersionMatch = isIos && UA.match(/os ([\d_]+)/);
+  var iosVersion = iosVersionMatch && iosVersionMatch[1].split('_');
+
+  // detecting iOS UIWebView by indexedDB
+  var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB;
+
+  var transitionProp = undefined;
+  var transitionEndEvent = undefined;
+  var animationProp = undefined;
+  var animationEndEvent = undefined;
+
+  // Transition property/event sniffing
+  if (inBrowser && !isIE9) {
+    var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined;
+    var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined;
+    transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition';
+    transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend';
+    animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation';
+    animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend';
+  }
+
+  /**
+   * Defer a task to execute it asynchronously. Ideally this
+   * should be executed as a microtask, so we leverage
+   * MutationObserver if it's available, and fallback to
+   * setTimeout(0).
+   *
+   * @param {Function} cb
+   * @param {Object} ctx
+   */
+
+  var nextTick = (function () {
+    var callbacks = [];
+    var pending = false;
+    var timerFunc;
+    function nextTickHandler() {
+      pending = false;
+      var copies = callbacks.slice(0);
+      callbacks = [];
+      for (var i = 0; i < copies.length; i++) {
+        copies[i]();
+      }
+    }
+
+    /* istanbul ignore if */
+    if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
+      var counter = 1;
+      var observer = new MutationObserver(nextTickHandler);
+      var textNode = document.createTextNode(counter);
+      observer.observe(textNode, {
+        characterData: true
+      });
+      timerFunc = function () {
+        counter = (counter + 1) % 2;
+        textNode.data = counter;
+      };
+    } else {
+      // webpack attempts to inject a shim for setImmediate
+      // if it is used as a global, so we have to work around that to
+      // avoid bundling unnecessary code.
+      var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
+      timerFunc = context.setImmediate || setTimeout;
+    }
+    return function (cb, ctx) {
+      var func = ctx ? function () {
+        cb.call(ctx);
+      } : cb;
+      callbacks.push(func);
+      if (pending) return;
+      pending = true;
+      timerFunc(nextTickHandler, 0);
+    };
+  })();
+
+  var _Set = undefined;
+  /* istanbul ignore if */
+  if (typeof Set !== 'undefined' && Set.toString().match(/native code/)) {
+    // use native Set when available.
+    _Set = Set;
+  } else {
+    // a non-standard Set polyfill that only works with primitive keys.
+    _Set = function () {
+      this.set = Object.create(null);
+    };
+    _Set.prototype.has = function (key) {
+      return this.set[key] !== undefined;
+    };
+    _Set.prototype.add = function (key) {
+      this.set[key] = 1;
+    };
+    _Set.prototype.clear = function () {
+      this.set = Object.create(null);
+    };
+  }
+
+  function Cache(limit) {
+    this.size = 0;
+    this.limit = limit;
+    this.head = this.tail = undefined;
+    this._keymap = Object.create(null);
+  }
+
+  var p = Cache.prototype;
+
+  /**
+   * Put <value> into the cache associated with <key>.
+   * Returns the entry which was removed to make room for
+   * the new entry. Otherwise undefined is returned.
+   * (i.e. if there was enough room already).
+   *
+   * @param {String} key
+   * @param {*} value
+   * @return {Entry|undefined}
+   */
+
+  p.put = function (key, value) {
+    var removed;
+
+    var entry = this.get(key, true);
+    if (!entry) {
+      if (this.size === this.limit) {
+        removed = this.shift();
+      }
+      entry = {
+        key: key
+      };
+      this._keymap[key] = entry;
+      if (this.tail) {
+        this.tail.newer = entry;
+        entry.older = this.tail;
+      } else {
+        this.head = entry;
+      }
+      this.tail = entry;
+      this.size++;
+    }
+    entry.value = value;
+
+    return removed;
+  };
+
+  /**
+   * Purge the least recently used (oldest) entry from the
+   * cache. Returns the removed entry or undefined if the
+   * cache was empty.
+   */
+
+  p.shift = function () {
+    var entry = this.head;
+    if (entry) {
+      this.head = this.head.newer;
+      this.head.older = undefined;
+      entry.newer = entry.older = undefined;
+      this._keymap[entry.key] = undefined;
+      this.size--;
+    }
+    return entry;
+  };
+
+  /**
+   * Get and register recent use of <key>. Returns the value
+   * associated with <key> or undefined if not in cache.
+   *
+   * @param {String} key
+   * @param {Boolean} returnEntry
+   * @return {Entry|*}
+   */
+
+  p.get = function (key, returnEntry) {
+    var entry = this._keymap[key];
+    if (entry === undefined) return;
+    if (entry === this.tail) {
+      return returnEntry ? entry : entry.value;
+    }
+    // HEAD--------------TAIL
+    //   <.older   .newer>
+    //  <--- add direction --
+    //   A  B  C  <D>  E
+    if (entry.newer) {
+      if (entry === this.head) {
+        this.head = entry.newer;
+      }
+      entry.newer.older = entry.older; // C <-- E.
+    }
+    if (entry.older) {
+      entry.older.newer = entry.newer; // C. --> E
+    }
+    entry.newer = undefined; // D --x
+    entry.older = this.tail; // D. --> E
+    if (this.tail) {
+      this.tail.newer = entry; // E. <-- D
+    }
+    this.tail = entry;
+    return returnEntry ? entry : entry.value;
+  };
+
+  var cache$1 = new Cache(1000);
+  var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g;
+  var reservedArgRE = /^in$|^-?\d+/;
+
+  /**
+   * Parser state
+   */
+
+  var str;
+  var dir;
+  var c;
+  var prev;
+  var i;
+  var l;
+  var lastFilterIndex;
+  var inSingle;
+  var inDouble;
+  var curly;
+  var square;
+  var paren;
+  /**
+   * Push a filter to the current directive object
+   */
+
+  function pushFilter() {
+    var exp = str.slice(lastFilterIndex, i).trim();
+    var filter;
+    if (exp) {
+      filter = {};
+      var tokens = exp.match(filterTokenRE);
+      filter.name = tokens[0];
+      if (tokens.length > 1) {
+        filter.args = tokens.slice(1).map(processFilterArg);
+      }
+    }
+    if (filter) {
+      (dir.filters = dir.filters || []).push(filter);
+    }
+    lastFilterIndex = i + 1;
+  }
+
+  /**
+   * Check if an argument is dynamic and strip quotes.
+   *
+   * @param {String} arg
+   * @return {Object}
+   */
+
+  function processFilterArg(arg) {
+    if (reservedArgRE.test(arg)) {
+      return {
+        value: toNumber(arg),
+        dynamic: false
+      };
+    } else {
+      var stripped = stripQuotes(arg);
+      var dynamic = stripped === arg;
+      return {
+        value: dynamic ? arg : stripped,
+        dynamic: dynamic
+      };
+    }
+  }
+
+  /**
+   * Parse a directive value and extract the expression
+   * and its filters into a descriptor.
+   *
+   * Example:
+   *
+   * "a + 1 | uppercase" will yield:
+   * {
+   *   expression: 'a + 1',
+   *   filters: [
+   *     { name: 'uppercase', args: null }
+   *   ]
+   * }
+   *
+   * @param {String} s
+   * @return {Object}
+   */
+
+  function parseDirective(s) {
+    var hit = cache$1.get(s);
+    if (hit) {
+      return hit;
+    }
+
+    // reset parser state
+    str = s;
+    inSingle = inDouble = false;
+    curly = square = paren = 0;
+    lastFilterIndex = 0;
+    dir = {};
+
+    for (i = 0, l = str.length; i < l; i++) {
+      prev = c;
+      c = str.charCodeAt(i);
+      if (inSingle) {
+        // check single quote
+        if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
+      } else if (inDouble) {
+        // check double quote
+        if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
+      } else if (c === 0x7C && // pipe
+      str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
+        if (dir.expression == null) {
+          // first filter, end of expression
+          lastFilterIndex = i + 1;
+          dir.expression = str.slice(0, i).trim();
+        } else {
+          // already has filter
+          pushFilter();
+        }
+      } else {
+        switch (c) {
+          case 0x22:
+            inDouble = true;break; // "
+          case 0x27:
+            inSingle = true;break; // '
+          case 0x28:
+            paren++;break; // (
+          case 0x29:
+            paren--;break; // )
+          case 0x5B:
+            square++;break; // [
+          case 0x5D:
+            square--;break; // ]
+          case 0x7B:
+            curly++;break; // {
+          case 0x7D:
+            curly--;break; // }
+        }
+      }
+    }
+
+    if (dir.expression == null) {
+      dir.expression = str.slice(0, i).trim();
+    } else if (lastFilterIndex !== 0) {
+      pushFilter();
+    }
+
+    cache$1.put(s, dir);
+    return dir;
+  }
+
+var directive = Object.freeze({
+    parseDirective: parseDirective
+  });
+
+  var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
+  var cache = undefined;
+  var tagRE = undefined;
+  var htmlRE = undefined;
+  /**
+   * Escape a string so it can be used in a RegExp
+   * constructor.
+   *
+   * @param {String} str
+   */
+
+  function escapeRegex(str) {
+    return str.replace(regexEscapeRE, '\\$&');
+  }
+
+  function compileRegex() {
+    var open = escapeRegex(config.delimiters[0]);
+    var close = escapeRegex(config.delimiters[1]);
+    var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]);
+    var unsafeClose = escapeRegex(config.unsafeDelimiters[1]);
+    tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g');
+    htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$');
+    // reset cache
+    cache = new Cache(1000);
+  }
+
+  /**
+   * Parse a template text string into an array of tokens.
+   *
+   * @param {String} text
+   * @return {Array<Object> | null}
+   *               - {String} type
+   *               - {String} value
+   *               - {Boolean} [html]
+   *               - {Boolean} [oneTime]
+   */
+
+  function parseText(text) {
+    if (!cache) {
+      compileRegex();
+    }
+    var hit = cache.get(text);
+    if (hit) {
+      return hit;
+    }
+    if (!tagRE.test(text)) {
+      return null;
+    }
+    var tokens = [];
+    var lastIndex = tagRE.lastIndex = 0;
+    var match, index, html, value, first, oneTime;
+    /* eslint-disable no-cond-assign */
+    while (match = tagRE.exec(text)) {
+      /* eslint-enable no-cond-assign */
+      index = match.index;
+      // push text token
+      if (index > lastIndex) {
+        tokens.push({
+          value: text.slice(lastIndex, index)
+        });
+      }
+      // tag token
+      html = htmlRE.test(match[0]);
+      value = html ? match[1] : match[2];
+      first = value.charCodeAt(0);
+      oneTime = first === 42; // *
+      value = oneTime ? value.slice(1) : value;
+      tokens.push({
+        tag: true,
+        value: value.trim(),
+        html: html,
+        oneTime: oneTime
+      });
+      lastIndex = index + match[0].length;
+    }
+    if (lastIndex < text.length) {
+      tokens.push({
+        value: text.slice(lastIndex)
+      });
+    }
+    cache.put(text, tokens);
+    return tokens;
+  }
+
+  /**
+   * Format a list of tokens into an expression.
+   * e.g. tokens parsed from 'a {{b}} c' can be serialized
+   * into one single expression as '"a " + b + " c"'.
+   *
+   * @param {Array} tokens
+   * @param {Vue} [vm]
+   * @return {String}
+   */
+
+  function tokensToExp(tokens, vm) {
+    if (tokens.length > 1) {
+      return tokens.map(function (token) {
+        return formatToken(token, vm);
+      }).join('+');
+    } else {
+      return formatToken(tokens[0], vm, true);
+    }
+  }
+
+  /**
+   * Format a single token.
+   *
+   * @param {Object} token
+   * @param {Vue} [vm]
+   * @param {Boolean} [single]
+   * @return {String}
+   */
+
+  function formatToken(token, vm, single) {
+    return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"';
+  }
+
+  /**
+   * For an attribute with multiple interpolation tags,
+   * e.g. attr="some-{{thing | filter}}", in order to combine
+   * the whole thing into a single watchable expression, we
+   * have to inline those filters. This function does exactly
+   * that. This is a bit hacky but it avoids heavy changes
+   * to directive parser and watcher mechanism.
+   *
+   * @param {String} exp
+   * @param {Boolean} single
+   * @return {String}
+   */
+
+  var filterRE = /[^|]\|[^|]/;
+  function inlineFilters(exp, single) {
+    if (!filterRE.test(exp)) {
+      return single ? exp : '(' + exp + ')';
+    } else {
+      var dir = parseDirective(exp);
+      if (!dir.filters) {
+        return '(' + exp + ')';
+      } else {
+        return 'this._applyFilters(' + dir.expression + // value
+        ',null,' + // oldValue (null for read)
+        JSON.stringify(dir.filters) + // filter descriptors
+        ',false)'; // write?
+      }
+    }
+  }
+
+var text = Object.freeze({
+    compileRegex: compileRegex,
+    parseText: parseText,
+    tokensToExp: tokensToExp
+  });
+
+  var delimiters = ['{{', '}}'];
+  var unsafeDelimiters = ['{{{', '}}}'];
+
+  var config = Object.defineProperties({
+
+    /**
+     * Whether to print debug messages.
+     * Also enables stack trace for warnings.
+     *
+     * @type {Boolean}
+     */
+
+    debug: false,
+
+    /**
+     * Whether to suppress warnings.
+     *
+     * @type {Boolean}
+     */
+
+    silent: false,
+
+    /**
+     * Whether to use async rendering.
+     */
+
+    async: true,
+
+    /**
+     * Whether to warn against errors caught when evaluating
+     * expressions.
+     */
+
+    warnExpressionErrors: true,
+
+    /**
+     * Whether to allow devtools inspection.
+     * Disabled by default in production builds.
+     */
+
+    devtools: 'development' !== 'production',
+
+    /**
+     * Internal flag to indicate the delimiters have been
+     * changed.
+     *
+     * @type {Boolean}
+     */
+
+    _delimitersChanged: true,
+
+    /**
+     * List of asset types that a component can own.
+     *
+     * @type {Array}
+     */
+
+    _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'],
+
+    /**
+     * prop binding modes
+     */
+
+    _propBindingModes: {
+      ONE_WAY: 0,
+      TWO_WAY: 1,
+      ONE_TIME: 2
+    },
+
+    /**
+     * Max circular updates allowed in a batcher flush cycle.
+     */
+
+    _maxUpdateCount: 100
+
+  }, {
+    delimiters: { /**
+                   * Interpolation delimiters. Changing these would trigger
+                   * the text parser to re-compile the regular expressions.
+                   *
+                   * @type {Array<String>}
+                   */
+
+      get: function get() {
+        return delimiters;
+      },
+      set: function set(val) {
+        delimiters = val;
+        compileRegex();
+      },
+      configurable: true,
+      enumerable: true
+    },
+    unsafeDelimiters: {
+      get: function get() {
+        return unsafeDelimiters;
+      },
+      set: function set(val) {
+        unsafeDelimiters = val;
+        compileRegex();
+      },
+      configurable: true,
+      enumerable: true
+    }
+  });
+
+  var warn = undefined;
+  var formatComponentName = undefined;
+
+  if ('development' !== 'production') {
+    (function () {
+      var hasConsole = typeof console !== 'undefined';
+
+      warn = function (msg, vm) {
+        if (hasConsole && !config.silent) {
+          console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''));
+        }
+      };
+
+      formatComponentName = function (vm) {
+        var name = vm._isVue ? vm.$options.name : vm.name;
+        return name ? ' (found in component: <' + hyphenate(name) + '>)' : '';
+      };
+    })();
+  }
+
+  /**
+   * Append with transition.
+   *
+   * @param {Element} el
+   * @param {Element} target
+   * @param {Vue} vm
+   * @param {Function} [cb]
+   */
+
+  function appendWithTransition(el, target, vm, cb) {
+    applyTransition(el, 1, function () {
+      target.appendChild(el);
+    }, vm, cb);
+  }
+
+  /**
+   * InsertBefore with transition.
+   *
+   * @param {Element} el
+   * @param {Element} target
+   * @param {Vue} vm
+   * @param {Function} [cb]
+   */
+
+  function beforeWithTransition(el, target, vm, cb) {
+    applyTransition(el, 1, function () {
+      before(el, target);
+    }, vm, cb);
+  }
+
+  /**
+   * Remove with transition.
+   *
+   * @param {Element} el
+   * @param {Vue} vm
+   * @param {Function} [cb]
+   */
+
+  function removeWithTransition(el, vm, cb) {
+    applyTransition(el, -1, function () {
+      remove(el);
+    }, vm, cb);
+  }
+
+  /**
+   * Apply transitions with an operation callback.
+   *
+   * @param {Element} el
+   * @param {Number} direction
+   *                  1: enter
+   *                 -1: leave
+   * @param {Function} op - the actual DOM operation
+   * @param {Vue} vm
+   * @param {Function} [cb]
+   */
+
+  function applyTransition(el, direction, op, vm, cb) {
+    var transition = el.__v_trans;
+    if (!transition ||
+    // skip if there are no js hooks and CSS transition is
+    // not supported
+    !transition.hooks && !transitionEndEvent ||
+    // skip transitions for initial compile
+    !vm._isCompiled ||
+    // if the vm is being manipulated by a parent directive
+    // during the parent's compilation phase, skip the
+    // animation.
+    vm.$parent && !vm.$parent._isCompiled) {
+      op();
+      if (cb) cb();
+      return;
+    }
+    var action = direction > 0 ? 'enter' : 'leave';
+    transition[action](op, cb);
+  }
+
+var transition = Object.freeze({
+    appendWithTransition: appendWithTransition,
+    beforeWithTransition: beforeWithTransition,
+    removeWithTransition: removeWithTransition,
+    applyTransition: applyTransition
+  });
+
+  /**
+   * Query an element selector if it's not an element already.
+   *
+   * @param {String|Element} el
+   * @return {Element}
+   */
+
+  function query(el) {
+    if (typeof el === 'string') {
+      var selector = el;
+      el = document.querySelector(el);
+      if (!el) {
+        'development' !== 'production' && warn('Cannot find element: ' + selector);
+      }
+    }
+    return el;
+  }
+
+  /**
+   * Check if a node is in the document.
+   * Note: document.documentElement.contains should work here
+   * but always returns false for comment nodes in phantomjs,
+   * making unit tests difficult. This is fixed by doing the
+   * contains() check on the node's parentNode instead of
+   * the node itself.
+   *
+   * @param {Node} node
+   * @return {Boolean}
+   */
+
+  function inDoc(node) {
+    if (!node) return false;
+    var doc = node.ownerDocument.documentElement;
+    var parent = node.parentNode;
+    return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent));
+  }
+
+  /**
+   * Get and remove an attribute from a node.
+   *
+   * @param {Node} node
+   * @param {String} _attr
+   */
+
+  function getAttr(node, _attr) {
+    var val = node.getAttribute(_attr);
+    if (val !== null) {
+      node.removeAttribute(_attr);
+    }
+    return val;
+  }
+
+  /**
+   * Get an attribute with colon or v-bind: prefix.
+   *
+   * @param {Node} node
+   * @param {String} name
+   * @return {String|null}
+   */
+
+  function getBindAttr(node, name) {
+    var val = getAttr(node, ':' + name);
+    if (val === null) {
+      val = getAttr(node, 'v-bind:' + name);
+    }
+    return val;
+  }
+
+  /**
+   * Check the presence of a bind attribute.
+   *
+   * @param {Node} node
+   * @param {String} name
+   * @return {Boolean}
+   */
+
+  function hasBindAttr(node, name) {
+    return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name);
+  }
+
+  /**
+   * Insert el before target
+   *
+   * @param {Element} el
+   * @param {Element} target
+   */
+
+  function before(el, target) {
+    target.parentNode.insertBefore(el, target);
+  }
+
+  /**
+   * Insert el after target
+   *
+   * @param {Element} el
+   * @param {Element} target
+   */
+
+  function after(el, target) {
+    if (target.nextSibling) {
+      before(el, target.nextSibling);
+    } else {
+      target.parentNode.appendChild(el);
+    }
+  }
+
+  /**
+   * Remove el from DOM
+   *
+   * @param {Element} el
+   */
+
+  function remove(el) {
+    el.parentNode.removeChild(el);
+  }
+
+  /**
+   * Prepend el to target
+   *
+   * @param {Element} el
+   * @param {Element} target
+   */
+
+  function prepend(el, target) {
+    if (target.firstChild) {
+      before(el, target.firstChild);
+    } else {
+      target.appendChild(el);
+    }
+  }
+
+  /**
+   * Replace target with el
+   *
+   * @param {Element} target
+   * @param {Element} el
+   */
+
+  function replace(target, el) {
+    var parent = target.parentNode;
+    if (parent) {
+      parent.replaceChild(el, target);
+    }
+  }
+
+  /**
+   * Add event listener shorthand.
+   *
+   * @param {Element} el
+   * @param {String} event
+   * @param {Function} cb
+   * @param {Boolean} [useCapture]
+   */
+
+  function on(el, event, cb, useCapture) {
+    el.addEventListener(event, cb, useCapture);
+  }
+
+  /**
+   * Remove event listener shorthand.
+   *
+   * @param {Element} el
+   * @param {String} event
+   * @param {Function} cb
+   */
+
+  function off(el, event, cb) {
+    el.removeEventListener(event, cb);
+  }
+
+  /**
+   * For IE9 compat: when both class and :class are present
+   * getAttribute('class') returns wrong value...
+   *
+   * @param {Element} el
+   * @return {String}
+   */
+
+  function getClass(el) {
+    var classname = el.className;
+    if (typeof classname === 'object') {
+      classname = classname.baseVal || '';
+    }
+    return classname;
+  }
+
+  /**
+   * In IE9, setAttribute('class') will result in empty class
+   * if the element also has the :class attribute; However in
+   * PhantomJS, setting `className` does not work on SVG elements...
+   * So we have to do a conditional check here.
+   *
+   * @param {Element} el
+   * @param {String} cls
+   */
+
+  function setClass(el, cls) {
+    /* istanbul ignore if */
+    if (isIE9 && !/svg$/.test(el.namespaceURI)) {
+      el.className = cls;
+    } else {
+      el.setAttribute('class', cls);
+    }
+  }
+
+  /**
+   * Add class with compatibility for IE & SVG
+   *
+   * @param {Element} el
+   * @param {String} cls
+   */
+
+  function addClass(el, cls) {
+    if (el.classList) {
+      el.classList.add(cls);
+    } else {
+      var cur = ' ' + getClass(el) + ' ';
+      if (cur.indexOf(' ' + cls + ' ') < 0) {
+        setClass(el, (cur + cls).trim());
+      }
+    }
+  }
+
+  /**
+   * Remove class with compatibility for IE & SVG
+   *
+   * @param {Element} el
+   * @param {String} cls
+   */
+
+  function removeClass(el, cls) {
+    if (el.classList) {
+      el.classList.remove(cls);
+    } else {
+      var cur = ' ' + getClass(el) + ' ';
+      var tar = ' ' + cls + ' ';
+      while (cur.indexOf(tar) >= 0) {
+        cur = cur.replace(tar, ' ');
+      }
+      setClass(el, cur.trim());
+    }
+    if (!el.className) {
+      el.removeAttribute('class');
+    }
+  }
+
+  /**
+   * Extract raw content inside an element into a temporary
+   * container div
+   *
+   * @param {Element} el
+   * @param {Boolean} asFragment
+   * @return {Element|DocumentFragment}
+   */
+
+  function extractContent(el, asFragment) {
+    var child;
+    var rawContent;
+    /* istanbul ignore if */
+    if (isTemplate(el) && isFragment(el.content)) {
+      el = el.content;
+    }
+    if (el.hasChildNodes()) {
+      trimNode(el);
+      rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div');
+      /* eslint-disable no-cond-assign */
+      while (child = el.firstChild) {
+        /* eslint-enable no-cond-assign */
+        rawContent.appendChild(child);
+      }
+    }
+    return rawContent;
+  }
+
+  /**
+   * Trim possible empty head/tail text and comment
+   * nodes inside a parent.
+   *
+   * @param {Node} node
+   */
+
+  function trimNode(node) {
+    var child;
+    /* eslint-disable no-sequences */
+    while ((child = node.firstChild, isTrimmable(child))) {
+      node.removeChild(child);
+    }
+    while ((child = node.lastChild, isTrimmable(child))) {
+      node.removeChild(child);
+    }
+    /* eslint-enable no-sequences */
+  }
+
+  function isTrimmable(node) {
+    return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8);
+  }
+
+  /**
+   * Check if an element is a template tag.
+   * Note if the template appears inside an SVG its tagName
+   * will be in lowercase.
+   *
+   * @param {Element} el
+   */
+
+  function isTemplate(el) {
+    return el.tagName && el.tagName.toLowerCase() === 'template';
+  }
+
+  /**
+   * Create an "anchor" for performing dom insertion/removals.
+   * This is used in a number of scenarios:
+   * - fragment instance
+   * - v-html
+   * - v-if
+   * - v-for
+   * - component
+   *
+   * @param {String} content
+   * @param {Boolean} persist - IE trashes empty textNodes on
+   *                            cloneNode(true), so in certain
+   *                            cases the anchor needs to be
+   *                            non-empty to be persisted in
+   *                            templates.
+   * @return {Comment|Text}
+   */
+
+  function createAnchor(content, persist) {
+    var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : '');
+    anchor.__v_anchor = true;
+    return anchor;
+  }
+
+  /**
+   * Find a component ref attribute that starts with $.
+   *
+   * @param {Element} node
+   * @return {String|undefined}
+   */
+
+  var refRE = /^v-ref:/;
+
+  function findRef(node) {
+    if (node.hasAttributes()) {
+      var attrs = node.attributes;
+      for (var i = 0, l = attrs.length; i < l; i++) {
+        var name = attrs[i].name;
+        if (refRE.test(name)) {
+          return camelize(name.replace(refRE, ''));
+        }
+      }
+    }
+  }
+
+  /**
+   * Map a function to a range of nodes .
+   *
+   * @param {Node} node
+   * @param {Node} end
+   * @param {Function} op
+   */
+
+  function mapNodeRange(node, end, op) {
+    var next;
+    while (node !== end) {
+      next = node.nextSibling;
+      op(node);
+      node = next;
+    }
+    op(end);
+  }
+
+  /**
+   * Remove a range of nodes with transition, store
+   * the nodes in a fragment with correct ordering,
+   * and call callback when done.
+   *
+   * @param {Node} start
+   * @param {Node} end
+   * @param {Vue} vm
+   * @param {DocumentFragment} frag
+   * @param {Function} cb
+   */
+
+  function removeNodeRange(start, end, vm, frag, cb) {
+    var done = false;
+    var removed = 0;
+    var nodes = [];
+    mapNodeRange(start, end, function (node) {
+      if (node === end) done = true;
+      nodes.push(node);
+      removeWithTransition(node, vm, onRemoved);
+    });
+    function onRemoved() {
+      removed++;
+      if (done && removed >= nodes.length) {
+        for (var i = 0; i < nodes.length; i++) {
+          frag.appendChild(nodes[i]);
+        }
+        cb && cb();
+      }
+    }
+  }
+
+  /**
+   * Check if a node is a DocumentFragment.
+   *
+   * @param {Node} node
+   * @return {Boolean}
+   */
+
+  function isFragment(node) {
+    return node && node.nodeType === 11;
+  }
+
+  /**
+   * Get outerHTML of elements, taking care
+   * of SVG elements in IE as well.
+   *
+   * @param {Element} el
+   * @return {String}
+   */
+
+  function getOuterHTML(el) {
+    if (el.outerHTML) {
+      return el.outerHTML;
+    } else {
+      var container = document.createElement('div');
+      container.appendChild(el.cloneNode(true));
+      return container.innerHTML;
+    }
+  }
+
+  var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i;
+  var reservedTagRE = /^(slot|partial|component)$/i;
+
+  var isUnknownElement = undefined;
+  if ('development' !== 'production') {
+    isUnknownElement = function (el, tag) {
+      if (tag.indexOf('-') > -1) {
+        // http://stackoverflow.com/a/28210364/1070244
+        return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement;
+      } else {
+        return (/HTMLUnknownElement/.test(el.toString()) &&
+          // Chrome returns unknown for several HTML5 elements.
+          // https://code.google.com/p/chromium/issues/detail?id=540526
+          // Firefox returns unknown for some "Interactive elements."
+          !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag)
+        );
+      }
+    };
+  }
+
+  /**
+   * Check if an element is a component, if yes return its
+   * component id.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Object|undefined}
+   */
+
+  function checkComponentAttr(el, options) {
+    var tag = el.tagName.toLowerCase();
+    var hasAttrs = el.hasAttributes();
+    if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
+      if (resolveAsset(options, 'components', tag)) {
+        return { id: tag };
+      } else {
+        var is = hasAttrs && getIsBinding(el, options);
+        if (is) {
+          return is;
+        } else if ('development' !== 'production') {
+          var expectedTag = options._componentNameMap && options._componentNameMap[tag];
+          if (expectedTag) {
+            warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
+          } else if (isUnknownElement(el, tag)) {
+            warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
+          }
+        }
+      }
+    } else if (hasAttrs) {
+      return getIsBinding(el, options);
+    }
+  }
+
+  /**
+   * Get "is" binding from an element.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Object|undefined}
+   */
+
+  function getIsBinding(el, options) {
+    // dynamic syntax
+    var exp = el.getAttribute('is');
+    if (exp != null) {
+      if (resolveAsset(options, 'components', exp)) {
+        el.removeAttribute('is');
+        return { id: exp };
+      }
+    } else {
+      exp = getBindAttr(el, 'is');
+      if (exp != null) {
+        return { id: exp, dynamic: true };
+      }
+    }
+  }
+
+  /**
+   * Option overwriting strategies are functions that handle
+   * how to merge a parent option value and a child option
+   * value into the final value.
+   *
+   * All strategy functions follow the same signature:
+   *
+   * @param {*} parentVal
+   * @param {*} childVal
+   * @param {Vue} [vm]
+   */
+
+  var strats = config.optionMergeStrategies = Object.create(null);
+
+  /**
+   * Helper that recursively merges two data objects together.
+   */
+
+  function mergeData(to, from) {
+    var key, toVal, fromVal;
+    for (key in from) {
+      toVal = to[key];
+      fromVal = from[key];
+      if (!hasOwn(to, key)) {
+        set(to, key, fromVal);
+      } else if (isObject(toVal) && isObject(fromVal)) {
+        mergeData(toVal, fromVal);
+      }
+    }
+    return to;
+  }
+
+  /**
+   * Data
+   */
+
+  strats.data = function (parentVal, childVal, vm) {
+    if (!vm) {
+      // in a Vue.extend merge, both should be functions
+      if (!childVal) {
+        return parentVal;
+      }
+      if (typeof childVal !== 'function') {
+        'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
+        return parentVal;
+      }
+      if (!parentVal) {
+        return childVal;
+      }
+      // when parentVal & childVal are both present,
+      // we need to return a function that returns the
+      // merged result of both functions... no need to
+      // check if parentVal is a function here because
+      // it has to be a function to pass previous merges.
+      return function mergedDataFn() {
+        return mergeData(childVal.call(this), parentVal.call(this));
+      };
+    } else if (parentVal || childVal) {
+      return function mergedInstanceDataFn() {
+        // instance merge
+        var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal;
+        var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined;
+        if (instanceData) {
+          return mergeData(instanceData, defaultData);
+        } else {
+          return defaultData;
+        }
+      };
+    }
+  };
+
+  /**
+   * El
+   */
+
+  strats.el = function (parentVal, childVal, vm) {
+    if (!vm && childVal && typeof childVal !== 'function') {
+      'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
+      return;
+    }
+    var ret = childVal || parentVal;
+    // invoke the element factory if this is instance merge
+    return vm && typeof ret === 'function' ? ret.call(vm) : ret;
+  };
+
+  /**
+   * Hooks and param attributes are merged as arrays.
+   */
+
+  strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) {
+    return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal;
+  };
+
+  /**
+   * Assets
+   *
+   * When a vm is present (instance creation), we need to do
+   * a three-way merge between constructor options, instance
+   * options and parent options.
+   */
+
+  function mergeAssets(parentVal, childVal) {
+    var res = Object.create(parentVal || null);
+    return childVal ? extend(res, guardArrayAssets(childVal)) : res;
+  }
+
+  config._assetTypes.forEach(function (type) {
+    strats[type + 's'] = mergeAssets;
+  });
+
+  /**
+   * Events & Watchers.
+   *
+   * Events & watchers hashes should not overwrite one
+   * another, so we merge them as arrays.
+   */
+
+  strats.watch = strats.events = function (parentVal, childVal) {
+    if (!childVal) return parentVal;
+    if (!parentVal) return childVal;
+    var ret = {};
+    extend(ret, parentVal);
+    for (var key in childVal) {
+      var parent = ret[key];
+      var child = childVal[key];
+      if (parent && !isArray(parent)) {
+        parent = [parent];
+      }
+      ret[key] = parent ? parent.concat(child) : [child];
+    }
+    return ret;
+  };
+
+  /**
+   * Other object hashes.
+   */
+
+  strats.props = strats.methods = strats.computed = function (parentVal, childVal) {
+    if (!childVal) return parentVal;
+    if (!parentVal) return childVal;
+    var ret = Object.create(null);
+    extend(ret, parentVal);
+    extend(ret, childVal);
+    return ret;
+  };
+
+  /**
+   * Default strategy.
+   */
+
+  var defaultStrat = function defaultStrat(parentVal, childVal) {
+    return childVal === undefined ? parentVal : childVal;
+  };
+
+  /**
+   * Make sure component options get converted to actual
+   * constructors.
+   *
+   * @param {Object} options
+   */
+
+  function guardComponents(options) {
+    if (options.components) {
+      var components = options.components = guardArrayAssets(options.components);
+      var ids = Object.keys(components);
+      var def;
+      if ('development' !== 'production') {
+        var map = options._componentNameMap = {};
+      }
+      for (var i = 0, l = ids.length; i < l; i++) {
+        var key = ids[i];
+        if (commonTagRE.test(key) || reservedTagRE.test(key)) {
+          'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
+          continue;
+        }
+        // record a all lowercase <-> kebab-case mapping for
+        // possible custom element case error warning
+        if ('development' !== 'production') {
+          map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
+        }
+        def = components[key];
+        if (isPlainObject(def)) {
+          components[key] = Vue.extend(def);
+        }
+      }
+    }
+  }
+
+  /**
+   * Ensure all props option syntax are normalized into the
+   * Object-based format.
+   *
+   * @param {Object} options
+   */
+
+  function guardProps(options) {
+    var props = options.props;
+    var i, val;
+    if (isArray(props)) {
+      options.props = {};
+      i = props.length;
+      while (i--) {
+        val = props[i];
+        if (typeof val === 'string') {
+          options.props[val] = null;
+        } else if (val.name) {
+          options.props[val.name] = val;
+        }
+      }
+    } else if (isPlainObject(props)) {
+      var keys = Object.keys(props);
+      i = keys.length;
+      while (i--) {
+        val = props[keys[i]];
+        if (typeof val === 'function') {
+          props[keys[i]] = { type: val };
+        }
+      }
+    }
+  }
+
+  /**
+   * Guard an Array-format assets option and converted it
+   * into the key-value Object format.
+   *
+   * @param {Object|Array} assets
+   * @return {Object}
+   */
+
+  function guardArrayAssets(assets) {
+    if (isArray(assets)) {
+      var res = {};
+      var i = assets.length;
+      var asset;
+      while (i--) {
+        asset = assets[i];
+        var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id;
+        if (!id) {
+          'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.');
+        } else {
+          res[id] = asset;
+        }
+      }
+      return res;
+    }
+    return assets;
+  }
+
+  /**
+   * Merge two option objects into a new one.
+   * Core utility used in both instantiation and inheritance.
+   *
+   * @param {Object} parent
+   * @param {Object} child
+   * @param {Vue} [vm] - if vm is present, indicates this is
+   *                     an instantiation merge.
+   */
+
+  function mergeOptions(parent, child, vm) {
+    guardComponents(child);
+    guardProps(child);
+    if ('development' !== 'production') {
+      if (child.propsData && !vm) {
+        warn('propsData can only be used as an instantiation option.');
+      }
+    }
+    var options = {};
+    var key;
+    if (child['extends']) {
+      parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
+    }
+    if (child.mixins) {
+      for (var i = 0, l = child.mixins.length; i < l; i++) {
+        var mixin = child.mixins[i];
+        var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
+        parent = mergeOptions(parent, mixinOptions, vm);
+      }
+    }
+    for (key in parent) {
+      mergeField(key);
+    }
+    for (key in child) {
+      if (!hasOwn(parent, key)) {
+        mergeField(key);
+      }
+    }
+    function mergeField(key) {
+      var strat = strats[key] || defaultStrat;
+      options[key] = strat(parent[key], child[key], vm, key);
+    }
+    return options;
+  }
+
+  /**
+   * Resolve an asset.
+   * This function is used because child instances need access
+   * to assets defined in its ancestor chain.
+   *
+   * @param {Object} options
+   * @param {String} type
+   * @param {String} id
+   * @param {Boolean} warnMissing
+   * @return {Object|Function}
+   */
+
+  function resolveAsset(options, type, id, warnMissing) {
+    /* istanbul ignore if */
+    if (typeof id !== 'string') {
+      return;
+    }
+    var assets = options[type];
+    var camelizedId;
+    var res = assets[id] ||
+    // camelCase ID
+    assets[camelizedId = camelize(id)] ||
+    // Pascal Case ID
+    assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
+    if ('development' !== 'production' && warnMissing && !res) {
+      warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
+    }
+    return res;
+  }
+
+  var uid$1 = 0;
+
+  /**
+   * A dep is an observable that can have multiple
+   * directives subscribing to it.
+   *
+   * @constructor
+   */
+  function Dep() {
+    this.id = uid$1++;
+    this.subs = [];
+  }
+
+  // the current target watcher being evaluated.
+  // this is globally unique because there could be only one
+  // watcher being evaluated at any time.
+  Dep.target = null;
+
+  /**
+   * Add a directive subscriber.
+   *
+   * @param {Directive} sub
+   */
+
+  Dep.prototype.addSub = function (sub) {
+    this.subs.push(sub);
+  };
+
+  /**
+   * Remove a directive subscriber.
+   *
+   * @param {Directive} sub
+   */
+
+  Dep.prototype.removeSub = function (sub) {
+    this.subs.$remove(sub);
+  };
+
+  /**
+   * Add self as a dependency to the target watcher.
+   */
+
+  Dep.prototype.depend = function () {
+    Dep.target.addDep(this);
+  };
+
+  /**
+   * Notify all subscribers of a new value.
+   */
+
+  Dep.prototype.notify = function () {
+    // stablize the subscriber list first
+    var subs = toArray(this.subs);
+    for (var i = 0, l = subs.length; i < l; i++) {
+      subs[i].update();
+    }
+  };
+
+  var arrayProto = Array.prototype;
+  var arrayMethods = Object.create(arrayProto)
+
+  /**
+   * Intercept mutating methods and emit events
+   */
+
+  ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
+    // cache original method
+    var original = arrayProto[method];
+    def(arrayMethods, method, function mutator() {
+      // avoid leaking arguments:
+      // http://jsperf.com/closure-with-arguments
+      var i = arguments.length;
+      var args = new Array(i);
+      while (i--) {
+        args[i] = arguments[i];
+      }
+      var result = original.apply(this, args);
+      var ob = this.__ob__;
+      var inserted;
+      switch (method) {
+        case 'push':
+          inserted = args;
+          break;
+        case 'unshift':
+          inserted = args;
+          break;
+        case 'splice':
+          inserted = args.slice(2);
+          break;
+      }
+      if (inserted) ob.observeArray(inserted);
+      // notify change
+      ob.dep.notify();
+      return result;
+    });
+  });
+
+  /**
+   * Swap the element at the given index with a new value
+   * and emits corresponding event.
+   *
+   * @param {Number} index
+   * @param {*} val
+   * @return {*} - replaced element
+   */
+
+  def(arrayProto, '$set', function $set(index, val) {
+    if (index >= this.length) {
+      this.length = Number(index) + 1;
+    }
+    return this.splice(index, 1, val)[0];
+  });
+
+  /**
+   * Convenience method to remove the element at given index or target element reference.
+   *
+   * @param {*} item
+   */
+
+  def(arrayProto, '$remove', function $remove(item) {
+    /* istanbul ignore if */
+    if (!this.length) return;
+    var index = indexOf(this, item);
+    if (index > -1) {
+      return this.splice(index, 1);
+    }
+  });
+
+  var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
+
+  /**
+   * By default, when a reactive property is set, the new value is
+   * also converted to become reactive. However in certain cases, e.g.
+   * v-for scope alias and props, we don't want to force conversion
+   * because the value may be a nested value under a frozen data structure.
+   *
+   * So whenever we want to set a reactive property without forcing
+   * conversion on the new value, we wrap that call inside this function.
+   */
+
+  var shouldConvert = true;
+
+  function withoutConversion(fn) {
+    shouldConvert = false;
+    fn();
+    shouldConvert = true;
+  }
+
+  /**
+   * Observer class that are attached to each observed
+   * object. Once attached, the observer converts target
+   * object's property keys into getter/setters that
+   * collect dependencies and dispatches updates.
+   *
+   * @param {Array|Object} value
+   * @constructor
+   */
+
+  function Observer(value) {
+    this.value = value;
+    this.dep = new Dep();
+    def(value, '__ob__', this);
+    if (isArray(value)) {
+      var augment = hasProto ? protoAugment : copyAugment;
+      augment(value, arrayMethods, arrayKeys);
+      this.observeArray(value);
+    } else {
+      this.walk(value);
+    }
+  }
+
+  // Instance methods
+
+  /**
+   * Walk through each property and convert them into
+   * getter/setters. This method should only be called when
+   * value type is Object.
+   *
+   * @param {Object} obj
+   */
+
+  Observer.prototype.walk = function (obj) {
+    var keys = Object.keys(obj);
+    for (var i = 0, l = keys.length; i < l; i++) {
+      this.convert(keys[i], obj[keys[i]]);
+    }
+  };
+
+  /**
+   * Observe a list of Array items.
+   *
+   * @param {Array} items
+   */
+
+  Observer.prototype.observeArray = function (items) {
+    for (var i = 0, l = items.length; i < l; i++) {
+      observe(items[i]);
+    }
+  };
+
+  /**
+   * Convert a property into getter/setter so we can emit
+   * the events when the property is accessed/changed.
+   *
+   * @param {String} key
+   * @param {*} val
+   */
+
+  Observer.prototype.convert = function (key, val) {
+    defineReactive(this.value, key, val);
+  };
+
+  /**
+   * Add an owner vm, so that when $set/$delete mutations
+   * happen we can notify owner vms to proxy the keys and
+   * digest the watchers. This is only called when the object
+   * is observed as an instance's root $data.
+   *
+   * @param {Vue} vm
+   */
+
+  Observer.prototype.addVm = function (vm) {
+    (this.vms || (this.vms = [])).push(vm);
+  };
+
+  /**
+   * Remove an owner vm. This is called when the object is
+   * swapped out as an instance's $data object.
+   *
+   * @param {Vue} vm
+   */
+
+  Observer.prototype.removeVm = function (vm) {
+    this.vms.$remove(vm);
+  };
+
+  // helpers
+
+  /**
+   * Augment an target Object or Array by intercepting
+   * the prototype chain using __proto__
+   *
+   * @param {Object|Array} target
+   * @param {Object} src
+   */
+
+  function protoAugment(target, src) {
+    /* eslint-disable no-proto */
+    target.__proto__ = src;
+    /* eslint-enable no-proto */
+  }
+
+  /**
+   * Augment an target Object or Array by defining
+   * hidden properties.
+   *
+   * @param {Object|Array} target
+   * @param {Object} proto
+   */
+
+  function copyAugment(target, src, keys) {
+    for (var i = 0, l = keys.length; i < l; i++) {
+      var key = keys[i];
+      def(target, key, src[key]);
+    }
+  }
+
+  /**
+   * Attempt to create an observer instance for a value,
+   * returns the new observer if successfully observed,
+   * or the existing observer if the value already has one.
+   *
+   * @param {*} value
+   * @param {Vue} [vm]
+   * @return {Observer|undefined}
+   * @static
+   */
+
+  function observe(value, vm) {
+    if (!value || typeof value !== 'object') {
+      return;
+    }
+    var ob;
+    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+      ob = value.__ob__;
+    } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
+      ob = new Observer(value);
+    }
+    if (ob && vm) {
+      ob.addVm(vm);
+    }
+    return ob;
+  }
+
+  /**
+   * Define a reactive property on an Object.
+   *
+   * @param {Object} obj
+   * @param {String} key
+   * @param {*} val
+   */
+
+  function defineReactive(obj, key, val) {
+    var dep = new Dep();
+
+    var property = Object.getOwnPropertyDescriptor(obj, key);
+    if (property && property.configurable === false) {
+      return;
+    }
+
+    // cater for pre-defined getter/setters
+    var getter = property && property.get;
+    var setter = property && property.set;
+
+    var childOb = observe(val);
+    Object.defineProperty(obj, key, {
+      enumerable: true,
+      configurable: true,
+      get: function reactiveGetter() {
+        var value = getter ? getter.call(obj) : val;
+        if (Dep.target) {
+          dep.depend();
+          if (childOb) {
+            childOb.dep.depend();
+          }
+          if (isArray(value)) {
+            for (var e, i = 0, l = value.length; i < l; i++) {
+              e = value[i];
+              e && e.__ob__ && e.__ob__.dep.depend();
+            }
+          }
+        }
+        return value;
+      },
+      set: function reactiveSetter(newVal) {
+        var value = getter ? getter.call(obj) : val;
+        if (newVal === value) {
+          return;
+        }
+        if (setter) {
+          setter.call(obj, newVal);
+        } else {
+          val = newVal;
+        }
+        childOb = observe(newVal);
+        dep.notify();
+      }
+    });
+  }
+
+
+
+  var util = Object.freeze({
+  	defineReactive: defineReactive,
+  	set: set,
+  	del: del,
+  	hasOwn: hasOwn,
+  	isLiteral: isLiteral,
+  	isReserved: isReserved,
+  	_toString: _toString,
+  	toNumber: toNumber,
+  	toBoolean: toBoolean,
+  	stripQuotes: stripQuotes,
+  	camelize: camelize,
+  	hyphenate: hyphenate,
+  	classify: classify,
+  	bind: bind,
+  	toArray: toArray,
+  	extend: extend,
+  	isObject: isObject,
+  	isPlainObject: isPlainObject,
+  	def: def,
+  	debounce: _debounce,
+  	indexOf: indexOf,
+  	cancellable: cancellable,
+  	looseEqual: looseEqual,
+  	isArray: isArray,
+  	hasProto: hasProto,
+  	inBrowser: inBrowser,
+  	devtools: devtools,
+  	isIE: isIE,
+  	isIE9: isIE9,
+  	isAndroid: isAndroid,
+  	isIos: isIos,
+  	iosVersionMatch: iosVersionMatch,
+  	iosVersion: iosVersion,
+  	hasMutationObserverBug: hasMutationObserverBug,
+  	get transitionProp () { return transitionProp; },
+  	get transitionEndEvent () { return transitionEndEvent; },
+  	get animationProp () { return animationProp; },
+  	get animationEndEvent () { return animationEndEvent; },
+  	nextTick: nextTick,
+  	get _Set () { return _Set; },
+  	query: query,
+  	inDoc: inDoc,
+  	getAttr: getAttr,
+  	getBindAttr: getBindAttr,
+  	hasBindAttr: hasBindAttr,
+  	before: before,
+  	after: after,
+  	remove: remove,
+  	prepend: prepend,
+  	replace: replace,
+  	on: on,
+  	off: off,
+  	setClass: setClass,
+  	addClass: addClass,
+  	removeClass: removeClass,
+  	extractContent: extractContent,
+  	trimNode: trimNode,
+  	isTemplate: isTemplate,
+  	createAnchor: createAnchor,
+  	findRef: findRef,
+  	mapNodeRange: mapNodeRange,
+  	removeNodeRange: removeNodeRange,
+  	isFragment: isFragment,
+  	getOuterHTML: getOuterHTML,
+  	mergeOptions: mergeOptions,
+  	resolveAsset: resolveAsset,
+  	checkComponentAttr: checkComponentAttr,
+  	commonTagRE: commonTagRE,
+  	reservedTagRE: reservedTagRE,
+  	get warn () { return warn; }
+  });
+
+  var uid = 0;
+
+  function initMixin (Vue) {
+    /**
+     * The main init sequence. This is called for every
+     * instance, including ones that are created from extended
+     * constructors.
+     *
+     * @param {Object} options - this options object should be
+     *                           the result of merging class
+     *                           options and the options passed
+     *                           in to the constructor.
+     */
+
+    Vue.prototype._init = function (options) {
+      options = options || {};
+
+      this.$el = null;
+      this.$parent = options.parent;
+      this.$root = this.$parent ? this.$parent.$root : this;
+      this.$children = [];
+      this.$refs = {}; // child vm references
+      this.$els = {}; // element references
+      this._watchers = []; // all watchers as an array
+      this._directives = []; // all directives
+
+      // a uid
+      this._uid = uid++;
+
+      // a flag to avoid this being observed
+      this._isVue = true;
+
+      // events bookkeeping
+      this._events = {}; // registered callbacks
+      this._eventsCount = {}; // for $broadcast optimization
+
+      // fragment instance properties
+      this._isFragment = false;
+      this._fragment = // @type {DocumentFragment}
+      this._fragmentStart = // @type {Text|Comment}
+      this._fragmentEnd = null; // @type {Text|Comment}
+
+      // lifecycle state
+      this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false;
+      this._unlinkFn = null;
+
+      // context:
+      // if this is a transcluded component, context
+      // will be the common parent vm of this instance
+      // and its host.
+      this._context = options._context || this.$parent;
+
+      // scope:
+      // if this is inside an inline v-for, the scope
+      // will be the intermediate scope created for this
+      // repeat fragment. this is used for linking props
+      // and container directives.
+      this._scope = options._scope;
+
+      // fragment:
+      // if this instance is compiled inside a Fragment, it
+      // needs to reigster itself as a child of that fragment
+      // for attach/detach to work properly.
+      this._frag = options._frag;
+      if (this._frag) {
+        this._frag.children.push(this);
+      }
+
+      // push self into parent / transclusion host
+      if (this.$parent) {
+        this.$parent.$children.push(this);
+      }
+
+      // merge options.
+      options = this.$options = mergeOptions(this.constructor.options, options, this);
+
+      // set ref
+      this._updateRef();
+
+      // initialize data as empty object.
+      // it will be filled up in _initData().
+      this._data = {};
+
+      // call init hook
+      this._callHook('init');
+
+      // initialize data observation and scope inheritance.
+      this._initState();
+
+      // setup event system and option events.
+      this._initEvents();
+
+      // call created hook
+      this._callHook('created');
+
+      // if `el` option is passed, start compilation.
+      if (options.el) {
+        this.$mount(options.el);
+      }
+    };
+  }
+
+  var pathCache = new Cache(1000);
+
+  // actions
+  var APPEND = 0;
+  var PUSH = 1;
+  var INC_SUB_PATH_DEPTH = 2;
+  var PUSH_SUB_PATH = 3;
+
+  // states
+  var BEFORE_PATH = 0;
+  var IN_PATH = 1;
+  var BEFORE_IDENT = 2;
+  var IN_IDENT = 3;
+  var IN_SUB_PATH = 4;
+  var IN_SINGLE_QUOTE = 5;
+  var IN_DOUBLE_QUOTE = 6;
+  var AFTER_PATH = 7;
+  var ERROR = 8;
+
+  var pathStateMachine = [];
+
+  pathStateMachine[BEFORE_PATH] = {
+    'ws': [BEFORE_PATH],
+    'ident': [IN_IDENT, APPEND],
+    '[': [IN_SUB_PATH],
+    'eof': [AFTER_PATH]
+  };
+
+  pathStateMachine[IN_PATH] = {
+    'ws': [IN_PATH],
+    '.': [BEFORE_IDENT],
+    '[': [IN_SUB_PATH],
+    'eof': [AFTER_PATH]
+  };
+
+  pathStateMachine[BEFORE_IDENT] = {
+    'ws': [BEFORE_IDENT],
+    'ident': [IN_IDENT, APPEND]
+  };
+
+  pathStateMachine[IN_IDENT] = {
+    'ident': [IN_IDENT, APPEND],
+    '0': [IN_IDENT, APPEND],
+    'number': [IN_IDENT, APPEND],
+    'ws': [IN_PATH, PUSH],
+    '.': [BEFORE_IDENT, PUSH],
+    '[': [IN_SUB_PATH, PUSH],
+    'eof': [AFTER_PATH, PUSH]
+  };
+
+  pathStateMachine[IN_SUB_PATH] = {
+    "'": [IN_SINGLE_QUOTE, APPEND],
+    '"': [IN_DOUBLE_QUOTE, APPEND],
+    '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
+    ']': [IN_PATH, PUSH_SUB_PATH],
+    'eof': ERROR,
+    'else': [IN_SUB_PATH, APPEND]
+  };
+
+  pathStateMachine[IN_SINGLE_QUOTE] = {
+    "'": [IN_SUB_PATH, APPEND],
+    'eof': ERROR,
+    'else': [IN_SINGLE_QUOTE, APPEND]
+  };
+
+  pathStateMachine[IN_DOUBLE_QUOTE] = {
+    '"': [IN_SUB_PATH, APPEND],
+    'eof': ERROR,
+    'else': [IN_DOUBLE_QUOTE, APPEND]
+  };
+
+  /**
+   * Determine the type of a character in a keypath.
+   *
+   * @param {Char} ch
+   * @return {String} type
+   */
+
+  function getPathCharType(ch) {
+    if (ch === undefined) {
+      return 'eof';
+    }
+
+    var code = ch.charCodeAt(0);
+
+    switch (code) {
+      case 0x5B: // [
+      case 0x5D: // ]
+      case 0x2E: // .
+      case 0x22: // "
+      case 0x27: // '
+      case 0x30:
+        // 0
+        return ch;
+
+      case 0x5F: // _
+      case 0x24:
+        // $
+        return 'ident';
+
+      case 0x20: // Space
+      case 0x09: // Tab
+      case 0x0A: // Newline
+      case 0x0D: // Return
+      case 0xA0: // No-break space
+      case 0xFEFF: // Byte Order Mark
+      case 0x2028: // Line Separator
+      case 0x2029:
+        // Paragraph Separator
+        return 'ws';
+    }
+
+    // a-z, A-Z
+    if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) {
+      return 'ident';
+    }
+
+    // 1-9
+    if (code >= 0x31 && code <= 0x39) {
+      return 'number';
+    }
+
+    return 'else';
+  }
+
+  /**
+   * Format a subPath, return its plain form if it is
+   * a literal string or number. Otherwise prepend the
+   * dynamic indicator (*).
+   *
+   * @param {String} path
+   * @return {String}
+   */
+
+  function formatSubPath(path) {
+    var trimmed = path.trim();
+    // invalid leading 0
+    if (path.charAt(0) === '0' && isNaN(path)) {
+      return false;
+    }
+    return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed;
+  }
+
+  /**
+   * Parse a string path into an array of segments
+   *
+   * @param {String} path
+   * @return {Array|undefined}
+   */
+
+  function parse(path) {
+    var keys = [];
+    var index = -1;
+    var mode = BEFORE_PATH;
+    var subPathDepth = 0;
+    var c, newChar, key, type, transition, action, typeMap;
+
+    var actions = [];
+
+    actions[PUSH] = function () {
+      if (key !== undefined) {
+        keys.push(key);
+        key = undefined;
+      }
+    };
+
+    actions[APPEND] = function () {
+      if (key === undefined) {
+        key = newChar;
+      } else {
+        key += newChar;
+      }
+    };
+
+    actions[INC_SUB_PATH_DEPTH] = function () {
+      actions[APPEND]();
+      subPathDepth++;
+    };
+
+    actions[PUSH_SUB_PATH] = function () {
+      if (subPathDepth > 0) {
+        subPathDepth--;
+        mode = IN_SUB_PATH;
+        actions[APPEND]();
+      } else {
+        subPathDepth = 0;
+        key = formatSubPath(key);
+        if (key === false) {
+          return false;
+        } else {
+          actions[PUSH]();
+        }
+      }
+    };
+
+    function maybeUnescapeQuote() {
+      var nextChar = path[index + 1];
+      if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') {
+        index++;
+        newChar = '\\' + nextChar;
+        actions[APPEND]();
+        return true;
+      }
+    }
+
+    while (mode != null) {
+      index++;
+      c = path[index];
+
+      if (c === '\\' && maybeUnescapeQuote()) {
+        continue;
+      }
+
+      type = getPathCharType(c);
+      typeMap = pathStateMachine[mode];
+      transition = typeMap[type] || typeMap['else'] || ERROR;
+
+      if (transition === ERROR) {
+        return; // parse error
+      }
+
+      mode = transition[0];
+      action = actions[transition[1]];
+      if (action) {
+        newChar = transition[2];
+        newChar = newChar === undefined ? c : newChar;
+        if (action() === false) {
+          return;
+        }
+      }
+
+      if (mode === AFTER_PATH) {
+        keys.raw = path;
+        return keys;
+      }
+    }
+  }
+
+  /**
+   * External parse that check for a cache hit first
+   *
+   * @param {String} path
+   * @return {Array|undefined}
+   */
+
+  function parsePath(path) {
+    var hit = pathCache.get(path);
+    if (!hit) {
+      hit = parse(path);
+      if (hit) {
+        pathCache.put(path, hit);
+      }
+    }
+    return hit;
+  }
+
+  /**
+   * Get from an object from a path string
+   *
+   * @param {Object} obj
+   * @param {String} path
+   */
+
+  function getPath(obj, path) {
+    return parseExpression(path).get(obj);
+  }
+
+  /**
+   * Warn against setting non-existent root path on a vm.
+   */
+
+  var warnNonExistent;
+  if ('development' !== 'production') {
+    warnNonExistent = function (path, vm) {
+      warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm);
+    };
+  }
+
+  /**
+   * Set on an object from a path
+   *
+   * @param {Object} obj
+   * @param {String | Array} path
+   * @param {*} val
+   */
+
+  function setPath(obj, path, val) {
+    var original = obj;
+    if (typeof path === 'string') {
+      path = parse(path);
+    }
+    if (!path || !isObject(obj)) {
+      return false;
+    }
+    var last, key;
+    for (var i = 0, l = path.length; i < l; i++) {
+      last = obj;
+      key = path[i];
+      if (key.charAt(0) === '*') {
+        key = parseExpression(key.slice(1)).get.call(original, original);
+      }
+      if (i < l - 1) {
+        obj = obj[key];
+        if (!isObject(obj)) {
+          obj = {};
+          if ('development' !== 'production' && last._isVue) {
+            warnNonExistent(path, last);
+          }
+          set(last, key, obj);
+        }
+      } else {
+        if (isArray(obj)) {
+          obj.$set(key, val);
+        } else if (key in obj) {
+          obj[key] = val;
+        } else {
+          if ('development' !== 'production' && obj._isVue) {
+            warnNonExistent(path, obj);
+          }
+          set(obj, key, val);
+        }
+      }
+    }
+    return true;
+  }
+
+var path = Object.freeze({
+    parsePath: parsePath,
+    getPath: getPath,
+    setPath: setPath
+  });
+
+  var expressionCache = new Cache(1000);
+
+  var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat';
+  var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)');
+
+  // keywords that don't make sense inside expressions
+  var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public';
+  var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)');
+
+  var wsRE = /\s/g;
+  var newlineRE = /\n/g;
+  var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g;
+  var restoreRE = /"(\d+)"/g;
+  var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
+  var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g;
+  var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/;
+
+  function noop() {}
+
+  /**
+   * Save / Rewrite / Restore
+   *
+   * When rewriting paths found in an expression, it is
+   * possible for the same letter sequences to be found in
+   * strings and Object literal property keys. Therefore we
+   * remove and store these parts in a temporary array, and
+   * restore them after the path rewrite.
+   */
+
+  var saved = [];
+
+  /**
+   * Save replacer
+   *
+   * The save regex can match two possible cases:
+   * 1. An opening object literal
+   * 2. A string
+   * If matched as a plain string, we need to escape its
+   * newlines, since the string needs to be preserved when
+   * generating the function body.
+   *
+   * @param {String} str
+   * @param {String} isString - str if matched as a string
+   * @return {String} - placeholder with index
+   */
+
+  function save(str, isString) {
+    var i = saved.length;
+    saved[i] = isString ? str.replace(newlineRE, '\\n') : str;
+    return '"' + i + '"';
+  }
+
+  /**
+   * Path rewrite replacer
+   *
+   * @param {String} raw
+   * @return {String}
+   */
+
+  function rewrite(raw) {
+    var c = raw.charAt(0);
+    var path = raw.slice(1);
+    if (allowedKeywordsRE.test(path)) {
+      return raw;
+    } else {
+      path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path;
+      return c + 'scope.' + path;
+    }
+  }
+
+  /**
+   * Restore replacer
+   *
+   * @param {String} str
+   * @param {String} i - matched save index
+   * @return {String}
+   */
+
+  function restore(str, i) {
+    return saved[i];
+  }
+
+  /**
+   * Rewrite an expression, prefixing all path accessors with
+   * `scope.` and generate getter/setter functions.
+   *
+   * @param {String} exp
+   * @return {Function}
+   */
+
+  function compileGetter(exp) {
+    if (improperKeywordsRE.test(exp)) {
+      'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp);
+    }
+    // reset state
+    saved.length = 0;
+    // save strings and object literal keys
+    var body = exp.replace(saveRE, save).replace(wsRE, '');
+    // rewrite all paths
+    // pad 1 space here because the regex matches 1 extra char
+    body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore);
+    return makeGetterFn(body);
+  }
+
+  /**
+   * Build a getter function. Requires eval.
+   *
+   * We isolate the try/catch so it doesn't affect the
+   * optimization of the parse function when it is not called.
+   *
+   * @param {String} body
+   * @return {Function|undefined}
+   */
+
+  function makeGetterFn(body) {
+    try {
+      /* eslint-disable no-new-func */
+      return new Function('scope', 'return ' + body + ';');
+      /* eslint-enable no-new-func */
+    } catch (e) {
+      if ('development' !== 'production') {
+        /* istanbul ignore if */
+        if (e.toString().match(/unsafe-eval|CSP/)) {
+          warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build');
+        } else {
+          warn('Invalid expression. ' + 'Generated function body: ' + body);
+        }
+      }
+      return noop;
+    }
+  }
+
+  /**
+   * Compile a setter function for the expression.
+   *
+   * @param {String} exp
+   * @return {Function|undefined}
+   */
+
+  function compileSetter(exp) {
+    var path = parsePath(exp);
+    if (path) {
+      return function (scope, val) {
+        setPath(scope, path, val);
+      };
+    } else {
+      'development' !== 'production' && warn('Invalid setter expression: ' + exp);
+    }
+  }
+
+  /**
+   * Parse an expression into re-written getter/setters.
+   *
+   * @param {String} exp
+   * @param {Boolean} needSet
+   * @return {Function}
+   */
+
+  function parseExpression(exp, needSet) {
+    exp = exp.trim();
+    // try cache
+    var hit = expressionCache.get(exp);
+    if (hit) {
+      if (needSet && !hit.set) {
+        hit.set = compileSetter(hit.exp);
+      }
+      return hit;
+    }
+    var res = { exp: exp };
+    res.get = isSimplePath(exp) && exp.indexOf('[') < 0
+    // optimized super simple getter
+    ? makeGetterFn('scope.' + exp)
+    // dynamic getter
+    : compileGetter(exp);
+    if (needSet) {
+      res.set = compileSetter(exp);
+    }
+    expressionCache.put(exp, res);
+    return res;
+  }
+
+  /**
+   * Check if an expression is a simple path.
+   *
+   * @param {String} exp
+   * @return {Boolean}
+   */
+
+  function isSimplePath(exp) {
+    return pathTestRE.test(exp) &&
+    // don't treat literal values as paths
+    !literalValueRE$1.test(exp) &&
+    // Math constants e.g. Math.PI, Math.E etc.
+    exp.slice(0, 5) !== 'Math.';
+  }
+
+var expression = Object.freeze({
+    parseExpression: parseExpression,
+    isSimplePath: isSimplePath
+  });
+
+  // we have two separate queues: one for directive updates
+  // and one for user watcher registered via $watch().
+  // we want to guarantee directive updates to be called
+  // before user watchers so that when user watchers are
+  // triggered, the DOM would have already been in updated
+  // state.
+
+  var queue = [];
+  var userQueue = [];
+  var has = {};
+  var circular = {};
+  var waiting = false;
+
+  /**
+   * Reset the batcher's state.
+   */
+
+  function resetBatcherState() {
+    queue.length = 0;
+    userQueue.length = 0;
+    has = {};
+    circular = {};
+    waiting = false;
+  }
+
+  /**
+   * Flush both queues and run the watchers.
+   */
+
+  function flushBatcherQueue() {
+    var _again = true;
+
+    _function: while (_again) {
+      _again = false;
+
+      runBatcherQueue(queue);
+      runBatcherQueue(userQueue);
+      // user watchers triggered more watchers,
+      // keep flushing until it depletes
+      if (queue.length) {
+        _again = true;
+        continue _function;
+      }
+      // dev tool hook
+      /* istanbul ignore if */
+      if (devtools && config.devtools) {
+        devtools.emit('flush');
+      }
+      resetBatcherState();
+    }
+  }
+
+  /**
+   * Run the watchers in a single queue.
+   *
+   * @param {Array} queue
+   */
+
+  function runBatcherQueue(queue) {
+    // do not cache length because more watchers might be pushed
+    // as we run existing watchers
+    for (var i = 0; i < queue.length; i++) {
+      var watcher = queue[i];
+      var id = watcher.id;
+      has[id] = null;
+      watcher.run();
+      // in dev build, check and stop circular updates.
+      if ('development' !== 'production' && has[id] != null) {
+        circular[id] = (circular[id] || 0) + 1;
+        if (circular[id] > config._maxUpdateCount) {
+          warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm);
+          break;
+        }
+      }
+    }
+    queue.length = 0;
+  }
+
+  /**
+   * Push a watcher into the watcher queue.
+   * Jobs with duplicate IDs will be skipped unless it's
+   * pushed when the queue is being flushed.
+   *
+   * @param {Watcher} watcher
+   *   properties:
+   *   - {Number} id
+   *   - {Function} run
+   */
+
+  function pushWatcher(watcher) {
+    var id = watcher.id;
+    if (has[id] == null) {
+      // push watcher into appropriate queue
+      var q = watcher.user ? userQueue : queue;
+      has[id] = q.length;
+      q.push(watcher);
+      // queue the flush
+      if (!waiting) {
+        waiting = true;
+        nextTick(flushBatcherQueue);
+      }
+    }
+  }
+
+  var uid$2 = 0;
+
+  /**
+   * A watcher parses an expression, collects dependencies,
+   * and fires callback when the expression value changes.
+   * This is used for both the $watch() api and directives.
+   *
+   * @param {Vue} vm
+   * @param {String|Function} expOrFn
+   * @param {Function} cb
+   * @param {Object} options
+   *                 - {Array} filters
+   *                 - {Boolean} twoWay
+   *                 - {Boolean} deep
+   *                 - {Boolean} user
+   *                 - {Boolean} sync
+   *                 - {Boolean} lazy
+   *                 - {Function} [preProcess]
+   *                 - {Function} [postProcess]
+   * @constructor
+   */
+  function Watcher(vm, expOrFn, cb, options) {
+    // mix in options
+    if (options) {
+      extend(this, options);
+    }
+    var isFn = typeof expOrFn === 'function';
+    this.vm = vm;
+    vm._watchers.push(this);
+    this.expression = expOrFn;
+    this.cb = cb;
+    this.id = ++uid$2; // uid for batching
+    this.active = true;
+    this.dirty = this.lazy; // for lazy watchers
+    this.deps = [];
+    this.newDeps = [];
+    this.depIds = new _Set();
+    this.newDepIds = new _Set();
+    this.prevError = null; // for async error stacks
+    // parse expression for getter/setter
+    if (isFn) {
+      this.getter = expOrFn;
+      this.setter = undefined;
+    } else {
+      var res = parseExpression(expOrFn, this.twoWay);
+      this.getter = res.get;
+      this.setter = res.set;
+    }
+    this.value = this.lazy ? undefined : this.get();
+    // state for avoiding false triggers for deep and Array
+    // watchers during vm._digest()
+    this.queued = this.shallow = false;
+  }
+
+  /**
+   * Evaluate the getter, and re-collect dependencies.
+   */
+
+  Watcher.prototype.get = function () {
+    this.beforeGet();
+    var scope = this.scope || this.vm;
+    var value;
+    try {
+      value = this.getter.call(scope, scope);
+    } catch (e) {
+      if ('development' !== 'production' && config.warnExpressionErrors) {
+        warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm);
+      }
+    }
+    // "touch" every property so they are all tracked as
+    // dependencies for deep watching
+    if (this.deep) {
+      traverse(value);
+    }
+    if (this.preProcess) {
+      value = this.preProcess(value);
+    }
+    if (this.filters) {
+      value = scope._applyFilters(value, null, this.filters, false);
+    }
+    if (this.postProcess) {
+      value = this.postProcess(value);
+    }
+    this.afterGet();
+    return value;
+  };
+
+  /**
+   * Set the corresponding value with the setter.
+   *
+   * @param {*} value
+   */
+
+  Watcher.prototype.set = function (value) {
+    var scope = this.scope || this.vm;
+    if (this.filters) {
+      value = scope._applyFilters(value, this.value, this.filters, true);
+    }
+    try {
+      this.setter.call(scope, scope, value);
+    } catch (e) {
+      if ('development' !== 'production' && config.warnExpressionErrors) {
+        warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm);
+      }
+    }
+    // two-way sync for v-for alias
+    var forContext = scope.$forContext;
+    if (forContext && forContext.alias === this.expression) {
+      if (forContext.filters) {
+        'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm);
+        return;
+      }
+      forContext._withLock(function () {
+        if (scope.$key) {
+          // original is an object
+          forContext.rawValue[scope.$key] = value;
+        } else {
+          forContext.rawValue.$set(scope.$index, value);
+        }
+      });
+    }
+  };
+
+  /**
+   * Prepare for dependency collection.
+   */
+
+  Watcher.prototype.beforeGet = function () {
+    Dep.target = this;
+  };
+
+  /**
+   * Add a dependency to this directive.
+   *
+   * @param {Dep} dep
+   */
+
+  Watcher.prototype.addDep = function (dep) {
+    var id = dep.id;
+    if (!this.newDepIds.has(id)) {
+      this.newDepIds.add(id);
+      this.newDeps.push(dep);
+      if (!this.depIds.has(id)) {
+        dep.addSub(this);
+      }
+    }
+  };
+
+  /**
+   * Clean up for dependency collection.
+   */
+
+  Watcher.prototype.afterGet = function () {
+    Dep.target = null;
+    var i = this.deps.length;
+    while (i--) {
+      var dep = this.deps[i];
+      if (!this.newDepIds.has(dep.id)) {
+        dep.removeSub(this);
+      }
+    }
+    var tmp = this.depIds;
+    this.depIds = this.newDepIds;
+    this.newDepIds = tmp;
+    this.newDepIds.clear();
+    tmp = this.deps;
+    this.deps = this.newDeps;
+    this.newDeps = tmp;
+    this.newDeps.length = 0;
+  };
+
+  /**
+   * Subscriber interface.
+   * Will be called when a dependency changes.
+   *
+   * @param {Boolean} shallow
+   */
+
+  Watcher.prototype.update = function (shallow) {
+    if (this.lazy) {
+      this.dirty = true;
+    } else if (this.sync || !config.async) {
+      this.run();
+    } else {
+      // if queued, only overwrite shallow with non-shallow,
+      // but not the other way around.
+      this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow;
+      this.queued = true;
+      // record before-push error stack in debug mode
+      /* istanbul ignore if */
+      if ('development' !== 'production' && config.debug) {
+        this.prevError = new Error('[vue] async stack trace');
+      }
+      pushWatcher(this);
+    }
+  };
+
+  /**
+   * Batcher job interface.
+   * Will be called by the batcher.
+   */
+
+  Watcher.prototype.run = function () {
+    if (this.active) {
+      var value = this.get();
+      if (value !== this.value ||
+      // Deep watchers and watchers on Object/Arrays should fire even
+      // when the value is the same, because the value may
+      // have mutated; but only do so if this is a
+      // non-shallow update (caused by a vm digest).
+      (isObject(value) || this.deep) && !this.shallow) {
+        // set new value
+        var oldValue = this.value;
+        this.value = value;
+        // in debug + async mode, when a watcher callbacks
+        // throws, we also throw the saved before-push error
+        // so the full cross-tick stack trace is available.
+        var prevError = this.prevError;
+        /* istanbul ignore if */
+        if ('development' !== 'production' && config.debug && prevError) {
+          this.prevError = null;
+          try {
+            this.cb.call(this.vm, value, oldValue);
+          } catch (e) {
+            nextTick(function () {
+              throw prevError;
+            }, 0);
+            throw e;
+          }
+        } else {
+          this.cb.call(this.vm, value, oldValue);
+        }
+      }
+      this.queued = this.shallow = false;
+    }
+  };
+
+  /**
+   * Evaluate the value of the watcher.
+   * This only gets called for lazy watchers.
+   */
+
+  Watcher.prototype.evaluate = function () {
+    // avoid overwriting another watcher that is being
+    // collected.
+    var current = Dep.target;
+    this.value = this.get();
+    this.dirty = false;
+    Dep.target = current;
+  };
+
+  /**
+   * Depend on all deps collected by this watcher.
+   */
+
+  Watcher.prototype.depend = function () {
+    var i = this.deps.length;
+    while (i--) {
+      this.deps[i].depend();
+    }
+  };
+
+  /**
+   * Remove self from all dependencies' subcriber list.
+   */
+
+  Watcher.prototype.teardown = function () {
+    if (this.active) {
+      // remove self from vm's watcher list
+      // this is a somewhat expensive operation so we skip it
+      // if the vm is being destroyed or is performing a v-for
+      // re-render (the watcher list is then filtered by v-for).
+      if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
+        this.vm._watchers.$remove(this);
+      }
+      var i = this.deps.length;
+      while (i--) {
+        this.deps[i].removeSub(this);
+      }
+      this.active = false;
+      this.vm = this.cb = this.value = null;
+    }
+  };
+
+  /**
+   * Recrusively traverse an object to evoke all converted
+   * getters, so that every nested property inside the object
+   * is collected as a "deep" dependency.
+   *
+   * @param {*} val
+   */
+
+  var seenObjects = new _Set();
+  function traverse(val, seen) {
+    var i = undefined,
+        keys = undefined;
+    if (!seen) {
+      seen = seenObjects;
+      seen.clear();
+    }
+    var isA = isArray(val);
+    var isO = isObject(val);
+    if ((isA || isO) && Object.isExtensible(val)) {
+      if (val.__ob__) {
+        var depId = val.__ob__.dep.id;
+        if (seen.has(depId)) {
+          return;
+        } else {
+          seen.add(depId);
+        }
+      }
+      if (isA) {
+        i = val.length;
+        while (i--) traverse(val[i], seen);
+      } else if (isO) {
+        keys = Object.keys(val);
+        i = keys.length;
+        while (i--) traverse(val[keys[i]], seen);
+      }
+    }
+  }
+
+  var text$1 = {
+
+    bind: function bind() {
+      this.attr = this.el.nodeType === 3 ? 'data' : 'textContent';
+    },
+
+    update: function update(value) {
+      this.el[this.attr] = _toString(value);
+    }
+  };
+
+  var templateCache = new Cache(1000);
+  var idSelectorCache = new Cache(1000);
+
+  var map = {
+    efault: [0, '', ''],
+    legend: [1, '<fieldset>', '</fieldset>'],
+    tr: [2, '<table><tbody>', '</tbody></table>'],
+    col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>']
+  };
+
+  map.td = map.th = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+  map.option = map.optgroup = [1, '<select multiple="multiple">', '</select>'];
+
+  map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '<table>', '</table>'];
+
+  map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '<svg ' + 'xmlns="http://www.w3.org/2000/svg" ' + 'xmlns:xlink="http://www.w3.org/1999/xlink" ' + 'xmlns:ev="http://www.w3.org/2001/xml-events"' + 'version="1.1">', '</svg>'];
+
+  /**
+   * Check if a node is a supported template node with a
+   * DocumentFragment content.
+   *
+   * @param {Node} node
+   * @return {Boolean}
+   */
+
+  function isRealTemplate(node) {
+    return isTemplate(node) && isFragment(node.content);
+  }
+
+  var tagRE$1 = /<([\w:-]+)/;
+  var entityRE = /&#?\w+?;/;
+  var commentRE = /<!--/;
+
+  /**
+   * Convert a string template to a DocumentFragment.
+   * Determines correct wrapping by tag types. Wrapping
+   * strategy found in jQuery & component/domify.
+   *
+   * @param {String} templateString
+   * @param {Boolean} raw
+   * @return {DocumentFragment}
+   */
+
+  function stringToFragment(templateString, raw) {
+    // try a cache hit first
+    var cacheKey = raw ? templateString : templateString.trim();
+    var hit = templateCache.get(cacheKey);
+    if (hit) {
+      return hit;
+    }
+
+    var frag = document.createDocumentFragment();
+    var tagMatch = templateString.match(tagRE$1);
+    var entityMatch = entityRE.test(templateString);
+    var commentMatch = commentRE.test(templateString);
+
+    if (!tagMatch && !entityMatch && !commentMatch) {
+      // text only, return a single text node.
+      frag.appendChild(document.createTextNode(templateString));
+    } else {
+      var tag = tagMatch && tagMatch[1];
+      var wrap = map[tag] || map.efault;
+      var depth = wrap[0];
+      var prefix = wrap[1];
+      var suffix = wrap[2];
+      var node = document.createElement('div');
+
+      node.innerHTML = prefix + templateString + suffix;
+      while (depth--) {
+        node = node.lastChild;
+      }
+
+      var child;
+      /* eslint-disable no-cond-assign */
+      while (child = node.firstChild) {
+        /* eslint-enable no-cond-assign */
+        frag.appendChild(child);
+      }
+    }
+    if (!raw) {
+      trimNode(frag);
+    }
+    templateCache.put(cacheKey, frag);
+    return frag;
+  }
+
+  /**
+   * Convert a template node to a DocumentFragment.
+   *
+   * @param {Node} node
+   * @return {DocumentFragment}
+   */
+
+  function nodeToFragment(node) {
+    // if its a template tag and the browser supports it,
+    // its content is already a document fragment. However, iOS Safari has
+    // bug when using directly cloned template content with touch
+    // events and can cause crashes when the nodes are removed from DOM, so we
+    // have to treat template elements as string templates. (#2805)
+    /* istanbul ignore if */
+    if (isRealTemplate(node)) {
+      return stringToFragment(node.innerHTML);
+    }
+    // script template
+    if (node.tagName === 'SCRIPT') {
+      return stringToFragment(node.textContent);
+    }
+    // normal node, clone it to avoid mutating the original
+    var clonedNode = cloneNode(node);
+    var frag = document.createDocumentFragment();
+    var child;
+    /* eslint-disable no-cond-assign */
+    while (child = clonedNode.firstChild) {
+      /* eslint-enable no-cond-assign */
+      frag.appendChild(child);
+    }
+    trimNode(frag);
+    return frag;
+  }
+
+  // Test for the presence of the Safari template cloning bug
+  // https://bugs.webkit.org/showug.cgi?id=137755
+  var hasBrokenTemplate = (function () {
+    /* istanbul ignore else */
+    if (inBrowser) {
+      var a = document.createElement('div');
+      a.innerHTML = '<template>1</template>';
+      return !a.cloneNode(true).firstChild.innerHTML;
+    } else {
+      return false;
+    }
+  })();
+
+  // Test for IE10/11 textarea placeholder clone bug
+  var hasTextareaCloneBug = (function () {
+    /* istanbul ignore else */
+    if (inBrowser) {
+      var t = document.createElement('textarea');
+      t.placeholder = 't';
+      return t.cloneNode(true).value === 't';
+    } else {
+      return false;
+    }
+  })();
+
+  /**
+   * 1. Deal with Safari cloning nested <template> bug by
+   *    manually cloning all template instances.
+   * 2. Deal with IE10/11 textarea placeholder bug by setting
+   *    the correct value after cloning.
+   *
+   * @param {Element|DocumentFragment} node
+   * @return {Element|DocumentFragment}
+   */
+
+  function cloneNode(node) {
+    /* istanbul ignore if */
+    if (!node.querySelectorAll) {
+      return node.cloneNode();
+    }
+    var res = node.cloneNode(true);
+    var i, original, cloned;
+    /* istanbul ignore if */
+    if (hasBrokenTemplate) {
+      var tempClone = res;
+      if (isRealTemplate(node)) {
+        node = node.content;
+        tempClone = res.content;
+      }
+      original = node.querySelectorAll('template');
+      if (original.length) {
+        cloned = tempClone.querySelectorAll('template');
+        i = cloned.length;
+        while (i--) {
+          cloned[i].parentNode.replaceChild(cloneNode(original[i]), cloned[i]);
+        }
+      }
+    }
+    /* istanbul ignore if */
+    if (hasTextareaCloneBug) {
+      if (node.tagName === 'TEXTAREA') {
+        res.value = node.value;
+      } else {
+        original = node.querySelectorAll('textarea');
+        if (original.length) {
+          cloned = res.querySelectorAll('textarea');
+          i = cloned.length;
+          while (i--) {
+            cloned[i].value = original[i].value;
+          }
+        }
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Process the template option and normalizes it into a
+   * a DocumentFragment that can be used as a partial or a
+   * instance template.
+   *
+   * @param {*} template
+   *        Possible values include:
+   *        - DocumentFragment object
+   *        - Node object of type Template
+   *        - id selector: '#some-template-id'
+   *        - template string: '<div><span>{{msg}}</span></div>'
+   * @param {Boolean} shouldClone
+   * @param {Boolean} raw
+   *        inline HTML interpolation. Do not check for id
+   *        selector and keep whitespace in the string.
+   * @return {DocumentFragment|undefined}
+   */
+
+  function parseTemplate(template, shouldClone, raw) {
+    var node, frag;
+
+    // if the template is already a document fragment,
+    // do nothing
+    if (isFragment(template)) {
+      trimNode(template);
+      return shouldClone ? cloneNode(template) : template;
+    }
+
+    if (typeof template === 'string') {
+      // id selector
+      if (!raw && template.charAt(0) === '#') {
+        // id selector can be cached too
+        frag = idSelectorCache.get(template);
+        if (!frag) {
+          node = document.getElementById(template.slice(1));
+          if (node) {
+            frag = nodeToFragment(node);
+            // save selector to cache
+            idSelectorCache.put(template, frag);
+          }
+        }
+      } else {
+        // normal string template
+        frag = stringToFragment(template, raw);
+      }
+    } else if (template.nodeType) {
+      // a direct node
+      frag = nodeToFragment(template);
+    }
+
+    return frag && shouldClone ? cloneNode(frag) : frag;
+  }
+
+var template = Object.freeze({
+    cloneNode: cloneNode,
+    parseTemplate: parseTemplate
+  });
+
+  var html = {
+
+    bind: function bind() {
+      // a comment node means this is a binding for
+      // {{{ inline unescaped html }}}
+      if (this.el.nodeType === 8) {
+        // hold nodes
+        this.nodes = [];
+        // replace the placeholder with proper anchor
+        this.anchor = createAnchor('v-html');
+        replace(this.el, this.anchor);
+      }
+    },
+
+    update: function update(value) {
+      value = _toString(value);
+      if (this.nodes) {
+        this.swap(value);
+      } else {
+        this.el.innerHTML = value;
+      }
+    },
+
+    swap: function swap(value) {
+      // remove old nodes
+      var i = this.nodes.length;
+      while (i--) {
+        remove(this.nodes[i]);
+      }
+      // convert new value to a fragment
+      // do not attempt to retrieve from id selector
+      var frag = parseTemplate(value, true, true);
+      // save a reference to these nodes so we can remove later
+      this.nodes = toArray(frag.childNodes);
+      before(frag, this.anchor);
+    }
+  };
+
+  /**
+   * Abstraction for a partially-compiled fragment.
+   * Can optionally compile content with a child scope.
+   *
+   * @param {Function} linker
+   * @param {Vue} vm
+   * @param {DocumentFragment} frag
+   * @param {Vue} [host]
+   * @param {Object} [scope]
+   * @param {Fragment} [parentFrag]
+   */
+  function Fragment(linker, vm, frag, host, scope, parentFrag) {
+    this.children = [];
+    this.childFrags = [];
+    this.vm = vm;
+    this.scope = scope;
+    this.inserted = false;
+    this.parentFrag = parentFrag;
+    if (parentFrag) {
+      parentFrag.childFrags.push(this);
+    }
+    this.unlink = linker(vm, frag, host, scope, this);
+    var single = this.single = frag.childNodes.length === 1 &&
+    // do not go single mode if the only node is an anchor
+    !frag.childNodes[0].__v_anchor;
+    if (single) {
+      this.node = frag.childNodes[0];
+      this.before = singleBefore;
+      this.remove = singleRemove;
+    } else {
+      this.node = createAnchor('fragment-start');
+      this.end = createAnchor('fragment-end');
+      this.frag = frag;
+      prepend(this.node, frag);
+      frag.appendChild(this.end);
+      this.before = multiBefore;
+      this.remove = multiRemove;
+    }
+    this.node.__v_frag = this;
+  }
+
+  /**
+   * Call attach/detach for all components contained within
+   * this fragment. Also do so recursively for all child
+   * fragments.
+   *
+   * @param {Function} hook
+   */
+
+  Fragment.prototype.callHook = function (hook) {
+    var i, l;
+    for (i = 0, l = this.childFrags.length; i < l; i++) {
+      this.childFrags[i].callHook(hook);
+    }
+    for (i = 0, l = this.children.length; i < l; i++) {
+      hook(this.children[i]);
+    }
+  };
+
+  /**
+   * Insert fragment before target, single node version
+   *
+   * @param {Node} target
+   * @param {Boolean} withTransition
+   */
+
+  function singleBefore(target, withTransition) {
+    this.inserted = true;
+    var method = withTransition !== false ? beforeWithTransition : before;
+    method(this.node, target, this.vm);
+    if (inDoc(this.node)) {
+      this.callHook(attach);
+    }
+  }
+
+  /**
+   * Remove fragment, single node version
+   */
+
+  function singleRemove() {
+    this.inserted = false;
+    var shouldCallRemove = inDoc(this.node);
+    var self = this;
+    this.beforeRemove();
+    removeWithTransition(this.node, this.vm, function () {
+      if (shouldCallRemove) {
+        self.callHook(detach);
+      }
+      self.destroy();
+    });
+  }
+
+  /**
+   * Insert fragment before target, multi-nodes version
+   *
+   * @param {Node} target
+   * @param {Boolean} withTransition
+   */
+
+  function multiBefore(target, withTransition) {
+    this.inserted = true;
+    var vm = this.vm;
+    var method = withTransition !== false ? beforeWithTransition : before;
+    mapNodeRange(this.node, this.end, function (node) {
+      method(node, target, vm);
+    });
+    if (inDoc(this.node)) {
+      this.callHook(attach);
+    }
+  }
+
+  /**
+   * Remove fragment, multi-nodes version
+   */
+
+  function multiRemove() {
+    this.inserted = false;
+    var self = this;
+    var shouldCallRemove = inDoc(this.node);
+    this.beforeRemove();
+    removeNodeRange(this.node, this.end, this.vm, this.frag, function () {
+      if (shouldCallRemove) {
+        self.callHook(detach);
+      }
+      self.destroy();
+    });
+  }
+
+  /**
+   * Prepare the fragment for removal.
+   */
+
+  Fragment.prototype.beforeRemove = function () {
+    var i, l;
+    for (i = 0, l = this.childFrags.length; i < l; i++) {
+      // call the same method recursively on child
+      // fragments, depth-first
+      this.childFrags[i].beforeRemove(false);
+    }
+    for (i = 0, l = this.children.length; i < l; i++) {
+      // Call destroy for all contained instances,
+      // with remove:false and defer:true.
+      // Defer is necessary because we need to
+      // keep the children to call detach hooks
+      // on them.
+      this.children[i].$destroy(false, true);
+    }
+    var dirs = this.unlink.dirs;
+    for (i = 0, l = dirs.length; i < l; i++) {
+      // disable the watchers on all the directives
+      // so that the rendered content stays the same
+      // during removal.
+      dirs[i]._watcher && dirs[i]._watcher.teardown();
+    }
+  };
+
+  /**
+   * Destroy the fragment.
+   */
+
+  Fragment.prototype.destroy = function () {
+    if (this.parentFrag) {
+      this.parentFrag.childFrags.$remove(this);
+    }
+    this.node.__v_frag = null;
+    this.unlink();
+  };
+
+  /**
+   * Call attach hook for a Vue instance.
+   *
+   * @param {Vue} child
+   */
+
+  function attach(child) {
+    if (!child._isAttached && inDoc(child.$el)) {
+      child._callHook('attached');
+    }
+  }
+
+  /**
+   * Call detach hook for a Vue instance.
+   *
+   * @param {Vue} child
+   */
+
+  function detach(child) {
+    if (child._isAttached && !inDoc(child.$el)) {
+      child._callHook('detached');
+    }
+  }
+
+  var linkerCache = new Cache(5000);
+
+  /**
+   * A factory that can be used to create instances of a
+   * fragment. Caches the compiled linker if possible.
+   *
+   * @param {Vue} vm
+   * @param {Element|String} el
+   */
+  function FragmentFactory(vm, el) {
+    this.vm = vm;
+    var template;
+    var isString = typeof el === 'string';
+    if (isString || isTemplate(el) && !el.hasAttribute('v-if')) {
+      template = parseTemplate(el, true);
+    } else {
+      template = document.createDocumentFragment();
+      template.appendChild(el);
+    }
+    this.template = template;
+    // linker can be cached, but only for components
+    var linker;
+    var cid = vm.constructor.cid;
+    if (cid > 0) {
+      var cacheId = cid + (isString ? el : getOuterHTML(el));
+      linker = linkerCache.get(cacheId);
+      if (!linker) {
+        linker = compile(template, vm.$options, true);
+        linkerCache.put(cacheId, linker);
+      }
+    } else {
+      linker = compile(template, vm.$options, true);
+    }
+    this.linker = linker;
+  }
+
+  /**
+   * Create a fragment instance with given host and scope.
+   *
+   * @param {Vue} host
+   * @param {Object} scope
+   * @param {Fragment} parentFrag
+   */
+
+  FragmentFactory.prototype.create = function (host, scope, parentFrag) {
+    var frag = cloneNode(this.template);
+    return new Fragment(this.linker, this.vm, frag, host, scope, parentFrag);
+  };
+
+  var ON = 700;
+  var MODEL = 800;
+  var BIND = 850;
+  var TRANSITION = 1100;
+  var EL = 1500;
+  var COMPONENT = 1500;
+  var PARTIAL = 1750;
+  var IF = 2100;
+  var FOR = 2200;
+  var SLOT = 2300;
+
+  var uid$3 = 0;
+
+  var vFor = {
+
+    priority: FOR,
+    terminal: true,
+
+    params: ['track-by', 'stagger', 'enter-stagger', 'leave-stagger'],
+
+    bind: function bind() {
+      // support "item in/of items" syntax
+      var inMatch = this.expression.match(/(.*) (?:in|of) (.*)/);
+      if (inMatch) {
+        var itMatch = inMatch[1].match(/\((.*),(.*)\)/);
+        if (itMatch) {
+          this.iterator = itMatch[1].trim();
+          this.alias = itMatch[2].trim();
+        } else {
+          this.alias = inMatch[1].trim();
+        }
+        this.expression = inMatch[2];
+      }
+
+      if (!this.alias) {
+        'development' !== 'production' && warn('Invalid v-for expression "' + this.descriptor.raw + '": ' + 'alias is required.', this.vm);
+        return;
+      }
+
+      // uid as a cache identifier
+      this.id = '__v-for__' + ++uid$3;
+
+      // check if this is an option list,
+      // so that we know if we need to update the <select>'s
+      // v-model when the option list has changed.
+      // because v-model has a lower priority than v-for,
+      // the v-model is not bound here yet, so we have to
+      // retrive it in the actual updateModel() function.
+      var tag = this.el.tagName;
+      this.isOption = (tag === 'OPTION' || tag === 'OPTGROUP') && this.el.parentNode.tagName === 'SELECT';
+
+      // setup anchor nodes
+      this.start = createAnchor('v-for-start');
+      this.end = createAnchor('v-for-end');
+      replace(this.el, this.end);
+      before(this.start, this.end);
+
+      // cache
+      this.cache = Object.create(null);
+
+      // fragment factory
+      this.factory = new FragmentFactory(this.vm, this.el);
+    },
+
+    update: function update(data) {
+      this.diff(data);
+      this.updateRef();
+      this.updateModel();
+    },
+
+    /**
+     * Diff, based on new data and old data, determine the
+     * minimum amount of DOM manipulations needed to make the
+     * DOM reflect the new data Array.
+     *
+     * The algorithm diffs the new data Array by storing a
+     * hidden reference to an owner vm instance on previously
+     * seen data. This allows us to achieve O(n) which is
+     * better than a levenshtein distance based algorithm,
+     * which is O(m * n).
+     *
+     * @param {Array} data
+     */
+
+    diff: function diff(data) {
+      // check if the Array was converted from an Object
+      var item = data[0];
+      var convertedFromObject = this.fromObject = isObject(item) && hasOwn(item, '$key') && hasOwn(item, '$value');
+
+      var trackByKey = this.params.trackBy;
+      var oldFrags = this.frags;
+      var frags = this.frags = new Array(data.length);
+      var alias = this.alias;
+      var iterator = this.iterator;
+      var start = this.start;
+      var end = this.end;
+      var inDocument = inDoc(start);
+      var init = !oldFrags;
+      var i, l, frag, key, value, primitive;
+
+      // First pass, go through the new Array and fill up
+      // the new frags array. If a piece of data has a cached
+      // instance for it, we reuse it. Otherwise build a new
+      // instance.
+      for (i = 0, l = data.length; i < l; i++) {
+        item = data[i];
+        key = convertedFromObject ? item.$key : null;
+        value = convertedFromObject ? item.$value : item;
+        primitive = !isObject(value);
+        frag = !init && this.getCachedFrag(value, i, key);
+        if (frag) {
+          // reusable fragment
+          frag.reused = true;
+          // update $index
+          frag.scope.$index = i;
+          // update $key
+          if (key) {
+            frag.scope.$key = key;
+          }
+          // update iterator
+          if (iterator) {
+            frag.scope[iterator] = key !== null ? key : i;
+          }
+          // update data for track-by, object repeat &
+          // primitive values.
+          if (trackByKey || convertedFromObject || primitive) {
+            withoutConversion(function () {
+              frag.scope[alias] = value;
+            });
+          }
+        } else {
+          // new isntance
+          frag = this.create(value, alias, i, key);
+          frag.fresh = !init;
+        }
+        frags[i] = frag;
+        if (init) {
+          frag.before(end);
+        }
+      }
+
+      // we're done for the initial render.
+      if (init) {
+        return;
+      }
+
+      // Second pass, go through the old fragments and
+      // destroy those who are not reused (and remove them
+      // from cache)
+      var removalIndex = 0;
+      var totalRemoved = oldFrags.length - frags.length;
+      // when removing a large number of fragments, watcher removal
+      // turns out to be a perf bottleneck, so we batch the watcher
+      // removals into a single filter call!
+      this.vm._vForRemoving = true;
+      for (i = 0, l = oldFrags.length; i < l; i++) {
+        frag = oldFrags[i];
+        if (!frag.reused) {
+          this.deleteCachedFrag(frag);
+          this.remove(frag, removalIndex++, totalRemoved, inDocument);
+        }
+      }
+      this.vm._vForRemoving = false;
+      if (removalIndex) {
+        this.vm._watchers = this.vm._watchers.filter(function (w) {
+          return w.active;
+        });
+      }
+
+      // Final pass, move/insert new fragments into the
+      // right place.
+      var targetPrev, prevEl, currentPrev;
+      var insertionIndex = 0;
+      for (i = 0, l = frags.length; i < l; i++) {
+        frag = frags[i];
+        // this is the frag that we should be after
+        targetPrev = frags[i - 1];
+        prevEl = targetPrev ? targetPrev.staggerCb ? targetPrev.staggerAnchor : targetPrev.end || targetPrev.node : start;
+        if (frag.reused && !frag.staggerCb) {
+          currentPrev = findPrevFrag(frag, start, this.id);
+          if (currentPrev !== targetPrev && (!currentPrev ||
+          // optimization for moving a single item.
+          // thanks to suggestions by @livoras in #1807
+          findPrevFrag(currentPrev, start, this.id) !== targetPrev)) {
+            this.move(frag, prevEl);
+          }
+        } else {
+          // new instance, or still in stagger.
+          // insert with updated stagger index.
+          this.insert(frag, insertionIndex++, prevEl, inDocument);
+        }
+        frag.reused = frag.fresh = false;
+      }
+    },
+
+    /**
+     * Create a new fragment instance.
+     *
+     * @param {*} value
+     * @param {String} alias
+     * @param {Number} index
+     * @param {String} [key]
+     * @return {Fragment}
+     */
+
+    create: function create(value, alias, index, key) {
+      var host = this._host;
+      // create iteration scope
+      var parentScope = this._scope || this.vm;
+      var scope = Object.create(parentScope);
+      // ref holder for the scope
+      scope.$refs = Object.create(parentScope.$refs);
+      scope.$els = Object.create(parentScope.$els);
+      // make sure point $parent to parent scope
+      scope.$parent = parentScope;
+      // for two-way binding on alias
+      scope.$forContext = this;
+      // define scope properties
+      // important: define the scope alias without forced conversion
+      // so that frozen data structures remain non-reactive.
+      withoutConversion(function () {
+        defineReactive(scope, alias, value);
+      });
+      defineReactive(scope, '$index', index);
+      if (key) {
+        defineReactive(scope, '$key', key);
+      } else if (scope.$key) {
+        // avoid accidental fallback
+        def(scope, '$key', null);
+      }
+      if (this.iterator) {
+        defineReactive(scope, this.iterator, key !== null ? key : index);
+      }
+      var frag = this.factory.create(host, scope, this._frag);
+      frag.forId = this.id;
+      this.cacheFrag(value, frag, index, key);
+      return frag;
+    },
+
+    /**
+     * Update the v-ref on owner vm.
+     */
+
+    updateRef: function updateRef() {
+      var ref = this.descriptor.ref;
+      if (!ref) return;
+      var hash = (this._scope || this.vm).$refs;
+      var refs;
+      if (!this.fromObject) {
+        refs = this.frags.map(findVmFromFrag);
+      } else {
+        refs = {};
+        this.frags.forEach(function (frag) {
+          refs[frag.scope.$key] = findVmFromFrag(frag);
+        });
+      }
+      hash[ref] = refs;
+    },
+
+    /**
+     * For option lists, update the containing v-model on
+     * parent <select>.
+     */
+
+    updateModel: function updateModel() {
+      if (this.isOption) {
+        var parent = this.start.parentNode;
+        var model = parent && parent.__v_model;
+        if (model) {
+          model.forceUpdate();
+        }
+      }
+    },
+
+    /**
+     * Insert a fragment. Handles staggering.
+     *
+     * @param {Fragment} frag
+     * @param {Number} index
+     * @param {Node} prevEl
+     * @param {Boolean} inDocument
+     */
+
+    insert: function insert(frag, index, prevEl, inDocument) {
+      if (frag.staggerCb) {
+        frag.staggerCb.cancel();
+        frag.staggerCb = null;
+      }
+      var staggerAmount = this.getStagger(frag, index, null, 'enter');
+      if (inDocument && staggerAmount) {
+        // create an anchor and insert it synchronously,
+        // so that we can resolve the correct order without
+        // worrying about some elements not inserted yet
+        var anchor = frag.staggerAnchor;
+        if (!anchor) {
+          anchor = frag.staggerAnchor = createAnchor('stagger-anchor');
+          anchor.__v_frag = frag;
+        }
+        after(anchor, prevEl);
+        var op = frag.staggerCb = cancellable(function () {
+          frag.staggerCb = null;
+          frag.before(anchor);
+          remove(anchor);
+        });
+        setTimeout(op, staggerAmount);
+      } else {
+        var target = prevEl.nextSibling;
+        /* istanbul ignore if */
+        if (!target) {
+          // reset end anchor position in case the position was messed up
+          // by an external drag-n-drop library.
+          after(this.end, prevEl);
+          target = this.end;
+        }
+        frag.before(target);
+      }
+    },
+
+    /**
+     * Remove a fragment. Handles staggering.
+     *
+     * @param {Fragment} frag
+     * @param {Number} index
+     * @param {Number} total
+     * @param {Boolean} inDocument
+     */
+
+    remove: function remove(frag, index, total, inDocument) {
+      if (frag.staggerCb) {
+        frag.staggerCb.cancel();
+        frag.staggerCb = null;
+        // it's not possible for the same frag to be removed
+        // twice, so if we have a pending stagger callback,
+        // it means this frag is queued for enter but removed
+        // before its transition started. Since it is already
+        // destroyed, we can just leave it in detached state.
+        return;
+      }
+      var staggerAmount = this.getStagger(frag, index, total, 'leave');
+      if (inDocument && staggerAmount) {
+        var op = frag.staggerCb = cancellable(function () {
+          frag.staggerCb = null;
+          frag.remove();
+        });
+        setTimeout(op, staggerAmount);
+      } else {
+        frag.remove();
+      }
+    },
+
+    /**
+     * Move a fragment to a new position.
+     * Force no transition.
+     *
+     * @param {Fragment} frag
+     * @param {Node} prevEl
+     */
+
+    move: function move(frag, prevEl) {
+      // fix a common issue with Sortable:
+      // if prevEl doesn't have nextSibling, this means it's
+      // been dragged after the end anchor. Just re-position
+      // the end anchor to the end of the container.
+      /* istanbul ignore if */
+      if (!prevEl.nextSibling) {
+        this.end.parentNode.appendChild(this.end);
+      }
+      frag.before(prevEl.nextSibling, false);
+    },
+
+    /**
+     * Cache a fragment using track-by or the object key.
+     *
+     * @param {*} value
+     * @param {Fragment} frag
+     * @param {Number} index
+     * @param {String} [key]
+     */
+
+    cacheFrag: function cacheFrag(value, frag, index, key) {
+      var trackByKey = this.params.trackBy;
+      var cache = this.cache;
+      var primitive = !isObject(value);
+      var id;
+      if (key || trackByKey || primitive) {
+        id = getTrackByKey(index, key, value, trackByKey);
+        if (!cache[id]) {
+          cache[id] = frag;
+        } else if (trackByKey !== '$index') {
+          'development' !== 'production' && this.warnDuplicate(value);
+        }
+      } else {
+        id = this.id;
+        if (hasOwn(value, id)) {
+          if (value[id] === null) {
+            value[id] = frag;
+          } else {
+            'development' !== 'production' && this.warnDuplicate(value);
+          }
+        } else if (Object.isExtensible(value)) {
+          def(value, id, frag);
+        } else if ('development' !== 'production') {
+          warn('Frozen v-for objects cannot be automatically tracked, make sure to ' + 'provide a track-by key.');
+        }
+      }
+      frag.raw = value;
+    },
+
+    /**
+     * Get a cached fragment from the value/index/key
+     *
+     * @param {*} value
+     * @param {Number} index
+     * @param {String} key
+     * @return {Fragment}
+     */
+
+    getCachedFrag: function getCachedFrag(value, index, key) {
+      var trackByKey = this.params.trackBy;
+      var primitive = !isObject(value);
+      var frag;
+      if (key || trackByKey || primitive) {
+        var id = getTrackByKey(index, key, value, trackByKey);
+        frag = this.cache[id];
+      } else {
+        frag = value[this.id];
+      }
+      if (frag && (frag.reused || frag.fresh)) {
+        'development' !== 'production' && this.warnDuplicate(value);
+      }
+      return frag;
+    },
+
+    /**
+     * Delete a fragment from cache.
+     *
+     * @param {Fragment} frag
+     */
+
+    deleteCachedFrag: function deleteCachedFrag(frag) {
+      var value = frag.raw;
+      var trackByKey = this.params.trackBy;
+      var scope = frag.scope;
+      var index = scope.$index;
+      // fix #948: avoid accidentally fall through to
+      // a parent repeater which happens to have $key.
+      var key = hasOwn(scope, '$key') && scope.$key;
+      var primitive = !isObject(value);
+      if (trackByKey || key || primitive) {
+        var id = getTrackByKey(index, key, value, trackByKey);
+        this.cache[id] = null;
+      } else {
+        value[this.id] = null;
+        frag.raw = null;
+      }
+    },
+
+    /**
+     * Get the stagger amount for an insertion/removal.
+     *
+     * @param {Fragment} frag
+     * @param {Number} index
+     * @param {Number} total
+     * @param {String} type
+     */
+
+    getStagger: function getStagger(frag, index, total, type) {
+      type = type + 'Stagger';
+      var trans = frag.node.__v_trans;
+      var hooks = trans && trans.hooks;
+      var hook = hooks && (hooks[type] || hooks.stagger);
+      return hook ? hook.call(frag, index, total) : index * parseInt(this.params[type] || this.params.stagger, 10);
+    },
+
+    /**
+     * Pre-process the value before piping it through the
+     * filters. This is passed to and called by the watcher.
+     */
+
+    _preProcess: function _preProcess(value) {
+      // regardless of type, store the un-filtered raw value.
+      this.rawValue = value;
+      return value;
+    },
+
+    /**
+     * Post-process the value after it has been piped through
+     * the filters. This is passed to and called by the watcher.
+     *
+     * It is necessary for this to be called during the
+     * watcher's dependency collection phase because we want
+     * the v-for to update when the source Object is mutated.
+     */
+
+    _postProcess: function _postProcess(value) {
+      if (isArray(value)) {
+        return value;
+      } else if (isPlainObject(value)) {
+        // convert plain object to array.
+        var keys = Object.keys(value);
+        var i = keys.length;
+        var res = new Array(i);
+        var key;
+        while (i--) {
+          key = keys[i];
+          res[i] = {
+            $key: key,
+            $value: value[key]
+          };
+        }
+        return res;
+      } else {
+        if (typeof value === 'number' && !isNaN(value)) {
+          value = range(value);
+        }
+        return value || [];
+      }
+    },
+
+    unbind: function unbind() {
+      if (this.descriptor.ref) {
+        (this._scope || this.vm).$refs[this.descriptor.ref] = null;
+      }
+      if (this.frags) {
+        var i = this.frags.length;
+        var frag;
+        while (i--) {
+          frag = this.frags[i];
+          this.deleteCachedFrag(frag);
+          frag.destroy();
+        }
+      }
+    }
+  };
+
+  /**
+   * Helper to find the previous element that is a fragment
+   * anchor. This is necessary because a destroyed frag's
+   * element could still be lingering in the DOM before its
+   * leaving transition finishes, but its inserted flag
+   * should have been set to false so we can skip them.
+   *
+   * If this is a block repeat, we want to make sure we only
+   * return frag that is bound to this v-for. (see #929)
+   *
+   * @param {Fragment} frag
+   * @param {Comment|Text} anchor
+   * @param {String} id
+   * @return {Fragment}
+   */
+
+  function findPrevFrag(frag, anchor, id) {
+    var el = frag.node.previousSibling;
+    /* istanbul ignore if */
+    if (!el) return;
+    frag = el.__v_frag;
+    while ((!frag || frag.forId !== id || !frag.inserted) && el !== anchor) {
+      el = el.previousSibling;
+      /* istanbul ignore if */
+      if (!el) return;
+      frag = el.__v_frag;
+    }
+    return frag;
+  }
+
+  /**
+   * Find a vm from a fragment.
+   *
+   * @param {Fragment} frag
+   * @return {Vue|undefined}
+   */
+
+  function findVmFromFrag(frag) {
+    var node = frag.node;
+    // handle multi-node frag
+    if (frag.end) {
+      while (!node.__vue__ && node !== frag.end && node.nextSibling) {
+        node = node.nextSibling;
+      }
+    }
+    return node.__vue__;
+  }
+
+  /**
+   * Create a range array from given number.
+   *
+   * @param {Number} n
+   * @return {Array}
+   */
+
+  function range(n) {
+    var i = -1;
+    var ret = new Array(Math.floor(n));
+    while (++i < n) {
+      ret[i] = i;
+    }
+    return ret;
+  }
+
+  /**
+   * Get the track by key for an item.
+   *
+   * @param {Number} index
+   * @param {String} key
+   * @param {*} value
+   * @param {String} [trackByKey]
+   */
+
+  function getTrackByKey(index, key, value, trackByKey) {
+    return trackByKey ? trackByKey === '$index' ? index : trackByKey.charAt(0).match(/\w/) ? getPath(value, trackByKey) : value[trackByKey] : key || value;
+  }
+
+  if ('development' !== 'production') {
+    vFor.warnDuplicate = function (value) {
+      warn('Duplicate value found in v-for="' + this.descriptor.raw + '": ' + JSON.stringify(value) + '. Use track-by="$index" if ' + 'you are expecting duplicate values.', this.vm);
+    };
+  }
+
+  var vIf = {
+
+    priority: IF,
+    terminal: true,
+
+    bind: function bind() {
+      var el = this.el;
+      if (!el.__vue__) {
+        // check else block
+        var next = el.nextElementSibling;
+        if (next && getAttr(next, 'v-else') !== null) {
+          remove(next);
+          this.elseEl = next;
+        }
+        // check main block
+        this.anchor = createAnchor('v-if');
+        replace(el, this.anchor);
+      } else {
+        'development' !== 'production' && warn('v-if="' + this.expression + '" cannot be ' + 'used on an instance root element.', this.vm);
+        this.invalid = true;
+      }
+    },
+
+    update: function update(value) {
+      if (this.invalid) return;
+      if (value) {
+        if (!this.frag) {
+          this.insert();
+        }
+      } else {
+        this.remove();
+      }
+    },
+
+    insert: function insert() {
+      if (this.elseFrag) {
+        this.elseFrag.remove();
+        this.elseFrag = null;
+      }
+      // lazy init factory
+      if (!this.factory) {
+        this.factory = new FragmentFactory(this.vm, this.el);
+      }
+      this.frag = this.factory.create(this._host, this._scope, this._frag);
+      this.frag.before(this.anchor);
+    },
+
+    remove: function remove() {
+      if (this.frag) {
+        this.frag.remove();
+        this.frag = null;
+      }
+      if (this.elseEl && !this.elseFrag) {
+        if (!this.elseFactory) {
+          this.elseFactory = new FragmentFactory(this.elseEl._context || this.vm, this.elseEl);
+        }
+        this.elseFrag = this.elseFactory.create(this._host, this._scope, this._frag);
+        this.elseFrag.before(this.anchor);
+      }
+    },
+
+    unbind: function unbind() {
+      if (this.frag) {
+        this.frag.destroy();
+      }
+      if (this.elseFrag) {
+        this.elseFrag.destroy();
+      }
+    }
+  };
+
+  var show = {
+
+    bind: function bind() {
+      // check else block
+      var next = this.el.nextElementSibling;
+      if (next && getAttr(next, 'v-else') !== null) {
+        this.elseEl = next;
+      }
+    },
+
+    update: function update(value) {
+      this.apply(this.el, value);
+      if (this.elseEl) {
+        this.apply(this.elseEl, !value);
+      }
+    },
+
+    apply: function apply(el, value) {
+      if (inDoc(el)) {
+        applyTransition(el, value ? 1 : -1, toggle, this.vm);
+      } else {
+        toggle();
+      }
+      function toggle() {
+        el.style.display = value ? '' : 'none';
+      }
+    }
+  };
+
+  var text$2 = {
+
+    bind: function bind() {
+      var self = this;
+      var el = this.el;
+      var isRange = el.type === 'range';
+      var lazy = this.params.lazy;
+      var number = this.params.number;
+      var debounce = this.params.debounce;
+
+      // handle composition events.
+      //   http://blog.evanyou.me/2014/01/03/composition-event/
+      // skip this for Android because it handles composition
+      // events quite differently. Android doesn't trigger
+      // composition events for language input methods e.g.
+      // Chinese, but instead triggers them for spelling
+      // suggestions... (see Discussion/#162)
+      var composing = false;
+      if (!isAndroid && !isRange) {
+        this.on('compositionstart', function () {
+          composing = true;
+        });
+        this.on('compositionend', function () {
+          composing = false;
+          // in IE11 the "compositionend" event fires AFTER
+          // the "input" event, so the input handler is blocked
+          // at the end... have to call it here.
+          //
+          // #1327: in lazy mode this is unecessary.
+          if (!lazy) {
+            self.listener();
+          }
+        });
+      }
+
+      // prevent messing with the input when user is typing,
+      // and force update on blur.
+      this.focused = false;
+      if (!isRange && !lazy) {
+        this.on('focus', function () {
+          self.focused = true;
+        });
+        this.on('blur', function () {
+          self.focused = false;
+          // do not sync value after fragment removal (#2017)
+          if (!self._frag || self._frag.inserted) {
+            self.rawListener();
+          }
+        });
+      }
+
+      // Now attach the main listener
+      this.listener = this.rawListener = function () {
+        if (composing || !self._bound) {
+          return;
+        }
+        var val = number || isRange ? toNumber(el.value) : el.value;
+        self.set(val);
+        // force update on next tick to avoid lock & same value
+        // also only update when user is not typing
+        nextTick(function () {
+          if (self._bound && !self.focused) {
+            self.update(self._watcher.value);
+          }
+        });
+      };
+
+      // apply debounce
+      if (debounce) {
+        this.listener = _debounce(this.listener, debounce);
+      }
+
+      // Support jQuery events, since jQuery.trigger() doesn't
+      // trigger native events in some cases and some plugins
+      // rely on $.trigger()
+      //
+      // We want to make sure if a listener is attached using
+      // jQuery, it is also removed with jQuery, that's why
+      // we do the check for each directive instance and
+      // store that check result on itself. This also allows
+      // easier test coverage control by unsetting the global
+      // jQuery variable in tests.
+      this.hasjQuery = typeof jQuery === 'function';
+      if (this.hasjQuery) {
+        var method = jQuery.fn.on ? 'on' : 'bind';
+        jQuery(el)[method]('change', this.rawListener);
+        if (!lazy) {
+          jQuery(el)[method]('input', this.listener);
+        }
+      } else {
+        this.on('change', this.rawListener);
+        if (!lazy) {
+          this.on('input', this.listener);
+        }
+      }
+
+      // IE9 doesn't fire input event on backspace/del/cut
+      if (!lazy && isIE9) {
+        this.on('cut', function () {
+          nextTick(self.listener);
+        });
+        this.on('keyup', function (e) {
+          if (e.keyCode === 46 || e.keyCode === 8) {
+            self.listener();
+          }
+        });
+      }
+
+      // set initial value if present
+      if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) {
+        this.afterBind = this.listener;
+      }
+    },
+
+    update: function update(value) {
+      // #3029 only update when the value changes. This prevent
+      // browsers from overwriting values like selectionStart
+      value = _toString(value);
+      if (value !== this.el.value) this.el.value = value;
+    },
+
+    unbind: function unbind() {
+      var el = this.el;
+      if (this.hasjQuery) {
+        var method = jQuery.fn.off ? 'off' : 'unbind';
+        jQuery(el)[method]('change', this.listener);
+        jQuery(el)[method]('input', this.listener);
+      }
+    }
+  };
+
+  var radio = {
+
+    bind: function bind() {
+      var self = this;
+      var el = this.el;
+
+      this.getValue = function () {
+        // value overwrite via v-bind:value
+        if (el.hasOwnProperty('_value')) {
+          return el._value;
+        }
+        var val = el.value;
+        if (self.params.number) {
+          val = toNumber(val);
+        }
+        return val;
+      };
+
+      this.listener = function () {
+        self.set(self.getValue());
+      };
+      this.on('change', this.listener);
+
+      if (el.hasAttribute('checked')) {
+        this.afterBind = this.listener;
+      }
+    },
+
+    update: function update(value) {
+      this.el.checked = looseEqual(value, this.getValue());
+    }
+  };
+
+  var select = {
+
+    bind: function bind() {
+      var _this = this;
+
+      var self = this;
+      var el = this.el;
+
+      // method to force update DOM using latest value.
+      this.forceUpdate = function () {
+        if (self._watcher) {
+          self.update(self._watcher.get());
+        }
+      };
+
+      // check if this is a multiple select
+      var multiple = this.multiple = el.hasAttribute('multiple');
+
+      // attach listener
+      this.listener = function () {
+        var value = getValue(el, multiple);
+        value = self.params.number ? isArray(value) ? value.map(toNumber) : toNumber(value) : value;
+        self.set(value);
+      };
+      this.on('change', this.listener);
+
+      // if has initial value, set afterBind
+      var initValue = getValue(el, multiple, true);
+      if (multiple && initValue.length || !multiple && initValue !== null) {
+        this.afterBind = this.listener;
+      }
+
+      // All major browsers except Firefox resets
+      // selectedIndex with value -1 to 0 when the element
+      // is appended to a new parent, therefore we have to
+      // force a DOM update whenever that happens...
+      this.vm.$on('hook:attached', function () {
+        nextTick(_this.forceUpdate);
+      });
+      if (!inDoc(el)) {
+        nextTick(this.forceUpdate);
+      }
+    },
+
+    update: function update(value) {
+      var el = this.el;
+      el.selectedIndex = -1;
+      var multi = this.multiple && isArray(value);
+      var options = el.options;
+      var i = options.length;
+      var op, val;
+      while (i--) {
+        op = options[i];
+        val = op.hasOwnProperty('_value') ? op._value : op.value;
+        /* eslint-disable eqeqeq */
+        op.selected = multi ? indexOf$1(value, val) > -1 : looseEqual(value, val);
+        /* eslint-enable eqeqeq */
+      }
+    },
+
+    unbind: function unbind() {
+      /* istanbul ignore next */
+      this.vm.$off('hook:attached', this.forceUpdate);
+    }
+  };
+
+  /**
+   * Get select value
+   *
+   * @param {SelectElement} el
+   * @param {Boolean} multi
+   * @param {Boolean} init
+   * @return {Array|*}
+   */
+
+  function getValue(el, multi, init) {
+    var res = multi ? [] : null;
+    var op, val, selected;
+    for (var i = 0, l = el.options.length; i < l; i++) {
+      op = el.options[i];
+      selected = init ? op.hasAttribute('selected') : op.selected;
+      if (selected) {
+        val = op.hasOwnProperty('_value') ? op._value : op.value;
+        if (multi) {
+          res.push(val);
+        } else {
+          return val;
+        }
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Native Array.indexOf uses strict equal, but in this
+   * case we need to match string/numbers with custom equal.
+   *
+   * @param {Array} arr
+   * @param {*} val
+   */
+
+  function indexOf$1(arr, val) {
+    var i = arr.length;
+    while (i--) {
+      if (looseEqual(arr[i], val)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  var checkbox = {
+
+    bind: function bind() {
+      var self = this;
+      var el = this.el;
+
+      this.getValue = function () {
+        return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
+      };
+
+      function getBooleanValue() {
+        var val = el.checked;
+        if (val && el.hasOwnProperty('_trueValue')) {
+          return el._trueValue;
+        }
+        if (!val && el.hasOwnProperty('_falseValue')) {
+          return el._falseValue;
+        }
+        return val;
+      }
+
+      this.listener = function () {
+        var model = self._watcher.value;
+        if (isArray(model)) {
+          var val = self.getValue();
+          if (el.checked) {
+            if (indexOf(model, val) < 0) {
+              model.push(val);
+            }
+          } else {
+            model.$remove(val);
+          }
+        } else {
+          self.set(getBooleanValue());
+        }
+      };
+
+      this.on('change', this.listener);
+      if (el.hasAttribute('checked')) {
+        this.afterBind = this.listener;
+      }
+    },
+
+    update: function update(value) {
+      var el = this.el;
+      if (isArray(value)) {
+        el.checked = indexOf(value, this.getValue()) > -1;
+      } else {
+        if (el.hasOwnProperty('_trueValue')) {
+          el.checked = looseEqual(value, el._trueValue);
+        } else {
+          el.checked = !!value;
+        }
+      }
+    }
+  };
+
+  var handlers = {
+    text: text$2,
+    radio: radio,
+    select: select,
+    checkbox: checkbox
+  };
+
+  var model = {
+
+    priority: MODEL,
+    twoWay: true,
+    handlers: handlers,
+    params: ['lazy', 'number', 'debounce'],
+
+    /**
+     * Possible elements:
+     *   <select>
+     *   <textarea>
+     *   <input type="*">
+     *     - text
+     *     - checkbox
+     *     - radio
+     *     - number
+     */
+
+    bind: function bind() {
+      // friendly warning...
+      this.checkFilters();
+      if (this.hasRead && !this.hasWrite) {
+        'development' !== 'production' && warn('It seems you are using a read-only filter with ' + 'v-model="' + this.descriptor.raw + '". ' + 'You might want to use a two-way filter to ensure correct behavior.', this.vm);
+      }
+      var el = this.el;
+      var tag = el.tagName;
+      var handler;
+      if (tag === 'INPUT') {
+        handler = handlers[el.type] || handlers.text;
+      } else if (tag === 'SELECT') {
+        handler = handlers.select;
+      } else if (tag === 'TEXTAREA') {
+        handler = handlers.text;
+      } else {
+        'development' !== 'production' && warn('v-model does not support element type: ' + tag, this.vm);
+        return;
+      }
+      el.__v_model = this;
+      handler.bind.call(this);
+      this.update = handler.update;
+      this._unbind = handler.unbind;
+    },
+
+    /**
+     * Check read/write filter stats.
+     */
+
+    checkFilters: function checkFilters() {
+      var filters = this.filters;
+      if (!filters) return;
+      var i = filters.length;
+      while (i--) {
+        var filter = resolveAsset(this.vm.$options, 'filters', filters[i].name);
+        if (typeof filter === 'function' || filter.read) {
+          this.hasRead = true;
+        }
+        if (filter.write) {
+          this.hasWrite = true;
+        }
+      }
+    },
+
+    unbind: function unbind() {
+      this.el.__v_model = null;
+      this._unbind && this._unbind();
+    }
+  };
+
+  // keyCode aliases
+  var keyCodes = {
+    esc: 27,
+    tab: 9,
+    enter: 13,
+    space: 32,
+    'delete': [8, 46],
+    up: 38,
+    left: 37,
+    right: 39,
+    down: 40
+  };
+
+  function keyFilter(handler, keys) {
+    var codes = keys.map(function (key) {
+      var charCode = key.charCodeAt(0);
+      if (charCode > 47 && charCode < 58) {
+        return parseInt(key, 10);
+      }
+      if (key.length === 1) {
+        charCode = key.toUpperCase().charCodeAt(0);
+        if (charCode > 64 && charCode < 91) {
+          return charCode;
+        }
+      }
+      return keyCodes[key];
+    });
+    codes = [].concat.apply([], codes);
+    return function keyHandler(e) {
+      if (codes.indexOf(e.keyCode) > -1) {
+        return handler.call(this, e);
+      }
+    };
+  }
+
+  function stopFilter(handler) {
+    return function stopHandler(e) {
+      e.stopPropagation();
+      return handler.call(this, e);
+    };
+  }
+
+  function preventFilter(handler) {
+    return function preventHandler(e) {
+      e.preventDefault();
+      return handler.call(this, e);
+    };
+  }
+
+  function selfFilter(handler) {
+    return function selfHandler(e) {
+      if (e.target === e.currentTarget) {
+        return handler.call(this, e);
+      }
+    };
+  }
+
+  var on$1 = {
+
+    priority: ON,
+    acceptStatement: true,
+    keyCodes: keyCodes,
+
+    bind: function bind() {
+      // deal with iframes
+      if (this.el.tagName === 'IFRAME' && this.arg !== 'load') {
+        var self = this;
+        this.iframeBind = function () {
+          on(self.el.contentWindow, self.arg, self.handler, self.modifiers.capture);
+        };
+        this.on('load', this.iframeBind);
+      }
+    },
+
+    update: function update(handler) {
+      // stub a noop for v-on with no value,
+      // e.g. @mousedown.prevent
+      if (!this.descriptor.raw) {
+        handler = function () {};
+      }
+
+      if (typeof handler !== 'function') {
+        'development' !== 'production' && warn('v-on:' + this.arg + '="' + this.expression + '" expects a function value, ' + 'got ' + handler, this.vm);
+        return;
+      }
+
+      // apply modifiers
+      if (this.modifiers.stop) {
+        handler = stopFilter(handler);
+      }
+      if (this.modifiers.prevent) {
+        handler = preventFilter(handler);
+      }
+      if (this.modifiers.self) {
+        handler = selfFilter(handler);
+      }
+      // key filter
+      var keys = Object.keys(this.modifiers).filter(function (key) {
+        return key !== 'stop' && key !== 'prevent' && key !== 'self' && key !== 'capture';
+      });
+      if (keys.length) {
+        handler = keyFilter(handler, keys);
+      }
+
+      this.reset();
+      this.handler = handler;
+
+      if (this.iframeBind) {
+        this.iframeBind();
+      } else {
+        on(this.el, this.arg, this.handler, this.modifiers.capture);
+      }
+    },
+
+    reset: function reset() {
+      var el = this.iframeBind ? this.el.contentWindow : this.el;
+      if (this.handler) {
+        off(el, this.arg, this.handler);
+      }
+    },
+
+    unbind: function unbind() {
+      this.reset();
+    }
+  };
+
+  var prefixes = ['-webkit-', '-moz-', '-ms-'];
+  var camelPrefixes = ['Webkit', 'Moz', 'ms'];
+  var importantRE = /!important;?$/;
+  var propCache = Object.create(null);
+
+  var testEl = null;
+
+  var style = {
+
+    deep: true,
+
+    update: function update(value) {
+      if (typeof value === 'string') {
+        this.el.style.cssText = value;
+      } else if (isArray(value)) {
+        this.handleObject(value.reduce(extend, {}));
+      } else {
+        this.handleObject(value || {});
+      }
+    },
+
+    handleObject: function handleObject(value) {
+      // cache object styles so that only changed props
+      // are actually updated.
+      var cache = this.cache || (this.cache = {});
+      var name, val;
+      for (name in cache) {
+        if (!(name in value)) {
+          this.handleSingle(name, null);
+          delete cache[name];
+        }
+      }
+      for (name in value) {
+        val = value[name];
+        if (val !== cache[name]) {
+          cache[name] = val;
+          this.handleSingle(name, val);
+        }
+      }
+    },
+
+    handleSingle: function handleSingle(prop, value) {
+      prop = normalize(prop);
+      if (!prop) return; // unsupported prop
+      // cast possible numbers/booleans into strings
+      if (value != null) value += '';
+      if (value) {
+        var isImportant = importantRE.test(value) ? 'important' : '';
+        if (isImportant) {
+          /* istanbul ignore if */
+          if ('development' !== 'production') {
+            warn('It\'s probably a bad idea to use !important with inline rules. ' + 'This feature will be deprecated in a future version of Vue.');
+          }
+          value = value.replace(importantRE, '').trim();
+          this.el.style.setProperty(prop.kebab, value, isImportant);
+        } else {
+          this.el.style[prop.camel] = value;
+        }
+      } else {
+        this.el.style[prop.camel] = '';
+      }
+    }
+
+  };
+
+  /**
+   * Normalize a CSS property name.
+   * - cache result
+   * - auto prefix
+   * - camelCase -> dash-case
+   *
+   * @param {String} prop
+   * @return {String}
+   */
+
+  function normalize(prop) {
+    if (propCache[prop]) {
+      return propCache[prop];
+    }
+    var res = prefix(prop);
+    propCache[prop] = propCache[res] = res;
+    return res;
+  }
+
+  /**
+   * Auto detect the appropriate prefix for a CSS property.
+   * https://gist.github.com/paulirish/523692
+   *
+   * @param {String} prop
+   * @return {String}
+   */
+
+  function prefix(prop) {
+    prop = hyphenate(prop);
+    var camel = camelize(prop);
+    var upper = camel.charAt(0).toUpperCase() + camel.slice(1);
+    if (!testEl) {
+      testEl = document.createElement('div');
+    }
+    var i = prefixes.length;
+    var prefixed;
+    if (camel !== 'filter' && camel in testEl.style) {
+      return {
+        kebab: prop,
+        camel: camel
+      };
+    }
+    while (i--) {
+      prefixed = camelPrefixes[i] + upper;
+      if (prefixed in testEl.style) {
+        return {
+          kebab: prefixes[i] + prop,
+          camel: prefixed
+        };
+      }
+    }
+  }
+
+  // xlink
+  var xlinkNS = 'http://www.w3.org/1999/xlink';
+  var xlinkRE = /^xlink:/;
+
+  // check for attributes that prohibit interpolations
+  var disallowedInterpAttrRE = /^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/;
+  // these attributes should also set their corresponding properties
+  // because they only affect the initial state of the element
+  var attrWithPropsRE = /^(?:value|checked|selected|muted)$/;
+  // these attributes expect enumrated values of "true" or "false"
+  // but are not boolean attributes
+  var enumeratedAttrRE = /^(?:draggable|contenteditable|spellcheck)$/;
+
+  // these attributes should set a hidden property for
+  // binding v-model to object values
+  var modelProps = {
+    value: '_value',
+    'true-value': '_trueValue',
+    'false-value': '_falseValue'
+  };
+
+  var bind$1 = {
+
+    priority: BIND,
+
+    bind: function bind() {
+      var attr = this.arg;
+      var tag = this.el.tagName;
+      // should be deep watch on object mode
+      if (!attr) {
+        this.deep = true;
+      }
+      // handle interpolation bindings
+      var descriptor = this.descriptor;
+      var tokens = descriptor.interp;
+      if (tokens) {
+        // handle interpolations with one-time tokens
+        if (descriptor.hasOneTime) {
+          this.expression = tokensToExp(tokens, this._scope || this.vm);
+        }
+
+        // only allow binding on native attributes
+        if (disallowedInterpAttrRE.test(attr) || attr === 'name' && (tag === 'PARTIAL' || tag === 'SLOT')) {
+          'development' !== 'production' && warn(attr + '="' + descriptor.raw + '": ' + 'attribute interpolation is not allowed in Vue.js ' + 'directives and special attributes.', this.vm);
+          this.el.removeAttribute(attr);
+          this.invalid = true;
+        }
+
+        /* istanbul ignore if */
+        if ('development' !== 'production') {
+          var raw = attr + '="' + descriptor.raw + '": ';
+          // warn src
+          if (attr === 'src') {
+            warn(raw + 'interpolation in "src" attribute will cause ' + 'a 404 request. Use v-bind:src instead.', this.vm);
+          }
+
+          // warn style
+          if (attr === 'style') {
+            warn(raw + 'interpolation in "style" attribute will cause ' + 'the attribute to be discarded in Internet Explorer. ' + 'Use v-bind:style instead.', this.vm);
+          }
+        }
+      }
+    },
+
+    update: function update(value) {
+      if (this.invalid) {
+        return;
+      }
+      var attr = this.arg;
+      if (this.arg) {
+        this.handleSingle(attr, value);
+      } else {
+        this.handleObject(value || {});
+      }
+    },
+
+    // share object handler with v-bind:class
+    handleObject: style.handleObject,
+
+    handleSingle: function handleSingle(attr, value) {
+      var el = this.el;
+      var interp = this.descriptor.interp;
+      if (this.modifiers.camel) {
+        attr = camelize(attr);
+      }
+      if (!interp && attrWithPropsRE.test(attr) && attr in el) {
+        var attrValue = attr === 'value' ? value == null // IE9 will set input.value to "null" for null...
+        ? '' : value : value;
+
+        if (el[attr] !== attrValue) {
+          el[attr] = attrValue;
+        }
+      }
+      // set model props
+      var modelProp = modelProps[attr];
+      if (!interp && modelProp) {
+        el[modelProp] = value;
+        // update v-model if present
+        var model = el.__v_model;
+        if (model) {
+          model.listener();
+        }
+      }
+      // do not set value attribute for textarea
+      if (attr === 'value' && el.tagName === 'TEXTAREA') {
+        el.removeAttribute(attr);
+        return;
+      }
+      // update attribute
+      if (enumeratedAttrRE.test(attr)) {
+        el.setAttribute(attr, value ? 'true' : 'false');
+      } else if (value != null && value !== false) {
+        if (attr === 'class') {
+          // handle edge case #1960:
+          // class interpolation should not overwrite Vue transition class
+          if (el.__v_trans) {
+            value += ' ' + el.__v_trans.id + '-transition';
+          }
+          setClass(el, value);
+        } else if (xlinkRE.test(attr)) {
+          el.setAttributeNS(xlinkNS, attr, value === true ? '' : value);
+        } else {
+          el.setAttribute(attr, value === true ? '' : value);
+        }
+      } else {
+        el.removeAttribute(attr);
+      }
+    }
+  };
+
+  var el = {
+
+    priority: EL,
+
+    bind: function bind() {
+      /* istanbul ignore if */
+      if (!this.arg) {
+        return;
+      }
+      var id = this.id = camelize(this.arg);
+      var refs = (this._scope || this.vm).$els;
+      if (hasOwn(refs, id)) {
+        refs[id] = this.el;
+      } else {
+        defineReactive(refs, id, this.el);
+      }
+    },
+
+    unbind: function unbind() {
+      var refs = (this._scope || this.vm).$els;
+      if (refs[this.id] === this.el) {
+        refs[this.id] = null;
+      }
+    }
+  };
+
+  var ref = {
+    bind: function bind() {
+      'development' !== 'production' && warn('v-ref:' + this.arg + ' must be used on a child ' + 'component. Found on <' + this.el.tagName.toLowerCase() + '>.', this.vm);
+    }
+  };
+
+  var cloak = {
+    bind: function bind() {
+      var el = this.el;
+      this.vm.$once('pre-hook:compiled', function () {
+        el.removeAttribute('v-cloak');
+      });
+    }
+  };
+
+  // must export plain object
+  var directives = {
+    text: text$1,
+    html: html,
+    'for': vFor,
+    'if': vIf,
+    show: show,
+    model: model,
+    on: on$1,
+    bind: bind$1,
+    el: el,
+    ref: ref,
+    cloak: cloak
+  };
+
+  var vClass = {
+
+    deep: true,
+
+    update: function update(value) {
+      if (!value) {
+        this.cleanup();
+      } else if (typeof value === 'string') {
+        this.setClass(value.trim().split(/\s+/));
+      } else {
+        this.setClass(normalize$1(value));
+      }
+    },
+
+    setClass: function setClass(value) {
+      this.cleanup(value);
+      for (var i = 0, l = value.length; i < l; i++) {
+        var val = value[i];
+        if (val) {
+          apply(this.el, val, addClass);
+        }
+      }
+      this.prevKeys = value;
+    },
+
+    cleanup: function cleanup(value) {
+      var prevKeys = this.prevKeys;
+      if (!prevKeys) return;
+      var i = prevKeys.length;
+      while (i--) {
+        var key = prevKeys[i];
+        if (!value || value.indexOf(key) < 0) {
+          apply(this.el, key, removeClass);
+        }
+      }
+    }
+  };
+
+  /**
+   * Normalize objects and arrays (potentially containing objects)
+   * into array of strings.
+   *
+   * @param {Object|Array<String|Object>} value
+   * @return {Array<String>}
+   */
+
+  function normalize$1(value) {
+    var res = [];
+    if (isArray(value)) {
+      for (var i = 0, l = value.length; i < l; i++) {
+        var _key = value[i];
+        if (_key) {
+          if (typeof _key === 'string') {
+            res.push(_key);
+          } else {
+            for (var k in _key) {
+              if (_key[k]) res.push(k);
+            }
+          }
+        }
+      }
+    } else if (isObject(value)) {
+      for (var key in value) {
+        if (value[key]) res.push(key);
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Add or remove a class/classes on an element
+   *
+   * @param {Element} el
+   * @param {String} key The class name. This may or may not
+   *                     contain a space character, in such a
+   *                     case we'll deal with multiple class
+   *                     names at once.
+   * @param {Function} fn
+   */
+
+  function apply(el, key, fn) {
+    key = key.trim();
+    if (key.indexOf(' ') === -1) {
+      fn(el, key);
+      return;
+    }
+    // The key contains one or more space characters.
+    // Since a class name doesn't accept such characters, we
+    // treat it as multiple classes.
+    var keys = key.split(/\s+/);
+    for (var i = 0, l = keys.length; i < l; i++) {
+      fn(el, keys[i]);
+    }
+  }
+
+  var component = {
+
+    priority: COMPONENT,
+
+    params: ['keep-alive', 'transition-mode', 'inline-template'],
+
+    /**
+     * Setup. Two possible usages:
+     *
+     * - static:
+     *   <comp> or <div v-component="comp">
+     *
+     * - dynamic:
+     *   <component :is="view">
+     */
+
+    bind: function bind() {
+      if (!this.el.__vue__) {
+        // keep-alive cache
+        this.keepAlive = this.params.keepAlive;
+        if (this.keepAlive) {
+          this.cache = {};
+        }
+        // check inline-template
+        if (this.params.inlineTemplate) {
+          // extract inline template as a DocumentFragment
+          this.inlineTemplate = extractContent(this.el, true);
+        }
+        // component resolution related state
+        this.pendingComponentCb = this.Component = null;
+        // transition related state
+        this.pendingRemovals = 0;
+        this.pendingRemovalCb = null;
+        // create a ref anchor
+        this.anchor = createAnchor('v-component');
+        replace(this.el, this.anchor);
+        // remove is attribute.
+        // this is removed during compilation, but because compilation is
+        // cached, when the component is used elsewhere this attribute
+        // will remain at link time.
+        this.el.removeAttribute('is');
+        this.el.removeAttribute(':is');
+        // remove ref, same as above
+        if (this.descriptor.ref) {
+          this.el.removeAttribute('v-ref:' + hyphenate(this.descriptor.ref));
+        }
+        // if static, build right now.
+        if (this.literal) {
+          this.setComponent(this.expression);
+        }
+      } else {
+        'development' !== 'production' && warn('cannot mount component "' + this.expression + '" ' + 'on already mounted element: ' + this.el);
+      }
+    },
+
+    /**
+     * Public update, called by the watcher in the dynamic
+     * literal scenario, e.g. <component :is="view">
+     */
+
+    update: function update(value) {
+      if (!this.literal) {
+        this.setComponent(value);
+      }
+    },
+
+    /**
+     * Switch dynamic components. May resolve the component
+     * asynchronously, and perform transition based on
+     * specified transition mode. Accepts a few additional
+     * arguments specifically for vue-router.
+     *
+     * The callback is called when the full transition is
+     * finished.
+     *
+     * @param {String} value
+     * @param {Function} [cb]
+     */
+
+    setComponent: function setComponent(value, cb) {
+      this.invalidatePending();
+      if (!value) {
+        // just remove current
+        this.unbuild(true);
+        this.remove(this.childVM, cb);
+        this.childVM = null;
+      } else {
+        var self = this;
+        this.resolveComponent(value, function () {
+          self.mountComponent(cb);
+        });
+      }
+    },
+
+    /**
+     * Resolve the component constructor to use when creating
+     * the child vm.
+     *
+     * @param {String|Function} value
+     * @param {Function} cb
+     */
+
+    resolveComponent: function resolveComponent(value, cb) {
+      var self = this;
+      this.pendingComponentCb = cancellable(function (Component) {
+        self.ComponentName = Component.options.name || (typeof value === 'string' ? value : null);
+        self.Component = Component;
+        cb();
+      });
+      this.vm._resolveComponent(value, this.pendingComponentCb);
+    },
+
+    /**
+     * Create a new instance using the current constructor and
+     * replace the existing instance. This method doesn't care
+     * whether the new component and the old one are actually
+     * the same.
+     *
+     * @param {Function} [cb]
+     */
+
+    mountComponent: function mountComponent(cb) {
+      // actual mount
+      this.unbuild(true);
+      var self = this;
+      var activateHooks = this.Component.options.activate;
+      var cached = this.getCached();
+      var newComponent = this.build();
+      if (activateHooks && !cached) {
+        this.waitingFor = newComponent;
+        callActivateHooks(activateHooks, newComponent, function () {
+          if (self.waitingFor !== newComponent) {
+            return;
+          }
+          self.waitingFor = null;
+          self.transition(newComponent, cb);
+        });
+      } else {
+        // update ref for kept-alive component
+        if (cached) {
+          newComponent._updateRef();
+        }
+        this.transition(newComponent, cb);
+      }
+    },
+
+    /**
+     * When the component changes or unbinds before an async
+     * constructor is resolved, we need to invalidate its
+     * pending callback.
+     */
+
+    invalidatePending: function invalidatePending() {
+      if (this.pendingComponentCb) {
+        this.pendingComponentCb.cancel();
+        this.pendingComponentCb = null;
+      }
+    },
+
+    /**
+     * Instantiate/insert a new child vm.
+     * If keep alive and has cached instance, insert that
+     * instance; otherwise build a new one and cache it.
+     *
+     * @param {Object} [extraOptions]
+     * @return {Vue} - the created instance
+     */
+
+    build: function build(extraOptions) {
+      var cached = this.getCached();
+      if (cached) {
+        return cached;
+      }
+      if (this.Component) {
+        // default options
+        var options = {
+          name: this.ComponentName,
+          el: cloneNode(this.el),
+          template: this.inlineTemplate,
+          // make sure to add the child with correct parent
+          // if this is a transcluded component, its parent
+          // should be the transclusion host.
+          parent: this._host || this.vm,
+          // if no inline-template, then the compiled
+          // linker can be cached for better performance.
+          _linkerCachable: !this.inlineTemplate,
+          _ref: this.descriptor.ref,
+          _asComponent: true,
+          _isRouterView: this._isRouterView,
+          // if this is a transcluded component, context
+          // will be the common parent vm of this instance
+          // and its host.
+          _context: this.vm,
+          // if this is inside an inline v-for, the scope
+          // will be the intermediate scope created for this
+          // repeat fragment. this is used for linking props
+          // and container directives.
+          _scope: this._scope,
+          // pass in the owner fragment of this component.
+          // this is necessary so that the fragment can keep
+          // track of its contained components in order to
+          // call attach/detach hooks for them.
+          _frag: this._frag
+        };
+        // extra options
+        // in 1.0.0 this is used by vue-router only
+        /* istanbul ignore if */
+        if (extraOptions) {
+          extend(options, extraOptions);
+        }
+        var child = new this.Component(options);
+        if (this.keepAlive) {
+          this.cache[this.Component.cid] = child;
+        }
+        /* istanbul ignore if */
+        if ('development' !== 'production' && this.el.hasAttribute('transition') && child._isFragment) {
+          warn('Transitions will not work on a fragment instance. ' + 'Template: ' + child.$options.template, child);
+        }
+        return child;
+      }
+    },
+
+    /**
+     * Try to get a cached instance of the current component.
+     *
+     * @return {Vue|undefined}
+     */
+
+    getCached: function getCached() {
+      return this.keepAlive && this.cache[this.Component.cid];
+    },
+
+    /**
+     * Teardown the current child, but defers cleanup so
+     * that we can separate the destroy and removal steps.
+     *
+     * @param {Boolean} defer
+     */
+
+    unbuild: function unbuild(defer) {
+      if (this.waitingFor) {
+        if (!this.keepAlive) {
+          this.waitingFor.$destroy();
+        }
+        this.waitingFor = null;
+      }
+      var child = this.childVM;
+      if (!child || this.keepAlive) {
+        if (child) {
+          // remove ref
+          child._inactive = true;
+          child._updateRef(true);
+        }
+        return;
+      }
+      // the sole purpose of `deferCleanup` is so that we can
+      // "deactivate" the vm right now and perform DOM removal
+      // later.
+      child.$destroy(false, defer);
+    },
+
+    /**
+     * Remove current destroyed child and manually do
+     * the cleanup after removal.
+     *
+     * @param {Function} cb
+     */
+
+    remove: function remove(child, cb) {
+      var keepAlive = this.keepAlive;
+      if (child) {
+        // we may have a component switch when a previous
+        // component is still being transitioned out.
+        // we want to trigger only one lastest insertion cb
+        // when the existing transition finishes. (#1119)
+        this.pendingRemovals++;
+        this.pendingRemovalCb = cb;
+        var self = this;
+        child.$remove(function () {
+          self.pendingRemovals--;
+          if (!keepAlive) child._cleanup();
+          if (!self.pendingRemovals && self.pendingRemovalCb) {
+            self.pendingRemovalCb();
+            self.pendingRemovalCb = null;
+          }
+        });
+      } else if (cb) {
+        cb();
+      }
+    },
+
+    /**
+     * Actually swap the components, depending on the
+     * transition mode. Defaults to simultaneous.
+     *
+     * @param {Vue} target
+     * @param {Function} [cb]
+     */
+
+    transition: function transition(target, cb) {
+      var self = this;
+      var current = this.childVM;
+      // for devtool inspection
+      if (current) current._inactive = true;
+      target._inactive = false;
+      this.childVM = target;
+      switch (self.params.transitionMode) {
+        case 'in-out':
+          target.$before(self.anchor, function () {
+            self.remove(current, cb);
+          });
+          break;
+        case 'out-in':
+          self.remove(current, function () {
+            target.$before(self.anchor, cb);
+          });
+          break;
+        default:
+          self.remove(current);
+          target.$before(self.anchor, cb);
+      }
+    },
+
+    /**
+     * Unbind.
+     */
+
+    unbind: function unbind() {
+      this.invalidatePending();
+      // Do not defer cleanup when unbinding
+      this.unbuild();
+      // destroy all keep-alive cached instances
+      if (this.cache) {
+        for (var key in this.cache) {
+          this.cache[key].$destroy();
+        }
+        this.cache = null;
+      }
+    }
+  };
+
+  /**
+   * Call activate hooks in order (asynchronous)
+   *
+   * @param {Array} hooks
+   * @param {Vue} vm
+   * @param {Function} cb
+   */
+
+  function callActivateHooks(hooks, vm, cb) {
+    var total = hooks.length;
+    var called = 0;
+    hooks[0].call(vm, next);
+    function next() {
+      if (++called >= total) {
+        cb();
+      } else {
+        hooks[called].call(vm, next);
+      }
+    }
+  }
+
+  var propBindingModes = config._propBindingModes;
+  var empty = {};
+
+  // regexes
+  var identRE$1 = /^[$_a-zA-Z]+[\w$]*$/;
+  var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/;
+
+  /**
+   * Compile props on a root element and return
+   * a props link function.
+   *
+   * @param {Element|DocumentFragment} el
+   * @param {Array} propOptions
+   * @param {Vue} vm
+   * @return {Function} propsLinkFn
+   */
+
+  function compileProps(el, propOptions, vm) {
+    var props = [];
+    var names = Object.keys(propOptions);
+    var i = names.length;
+    var options, name, attr, value, path, parsed, prop;
+    while (i--) {
+      name = names[i];
+      options = propOptions[name] || empty;
+
+      if ('development' !== 'production' && name === '$data') {
+        warn('Do not use $data as prop.', vm);
+        continue;
+      }
+
+      // props could contain dashes, which will be
+      // interpreted as minus calculations by the parser
+      // so we need to camelize the path here
+      path = camelize(name);
+      if (!identRE$1.test(path)) {
+        'development' !== 'production' && warn('Invalid prop key: "' + name + '". Prop keys ' + 'must be valid identifiers.', vm);
+        continue;
+      }
+
+      prop = {
+        name: name,
+        path: path,
+        options: options,
+        mode: propBindingModes.ONE_WAY,
+        raw: null
+      };
+
+      attr = hyphenate(name);
+      // first check dynamic version
+      if ((value = getBindAttr(el, attr)) === null) {
+        if ((value = getBindAttr(el, attr + '.sync')) !== null) {
+          prop.mode = propBindingModes.TWO_WAY;
+        } else if ((value = getBindAttr(el, attr + '.once')) !== null) {
+          prop.mode = propBindingModes.ONE_TIME;
+        }
+      }
+      if (value !== null) {
+        // has dynamic binding!
+        prop.raw = value;
+        parsed = parseDirective(value);
+        value = parsed.expression;
+        prop.filters = parsed.filters;
+        // check binding type
+        if (isLiteral(value) && !parsed.filters) {
+          // for expressions containing literal numbers and
+          // booleans, there's no need to setup a prop binding,
+          // so we can optimize them as a one-time set.
+          prop.optimizedLiteral = true;
+        } else {
+          prop.dynamic = true;
+          // check non-settable path for two-way bindings
+          if ('development' !== 'production' && prop.mode === propBindingModes.TWO_WAY && !settablePathRE.test(value)) {
+            prop.mode = propBindingModes.ONE_WAY;
+            warn('Cannot bind two-way prop with non-settable ' + 'parent path: ' + value, vm);
+          }
+        }
+        prop.parentPath = value;
+
+        // warn required two-way
+        if ('development' !== 'production' && options.twoWay && prop.mode !== propBindingModes.TWO_WAY) {
+          warn('Prop "' + name + '" expects a two-way binding type.', vm);
+        }
+      } else if ((value = getAttr(el, attr)) !== null) {
+        // has literal binding!
+        prop.raw = value;
+      } else if ('development' !== 'production') {
+        // check possible camelCase prop usage
+        var lowerCaseName = path.toLowerCase();
+        value = /[A-Z\-]/.test(name) && (el.getAttribute(lowerCaseName) || el.getAttribute(':' + lowerCaseName) || el.getAttribute('v-bind:' + lowerCaseName) || el.getAttribute(':' + lowerCaseName + '.once') || el.getAttribute('v-bind:' + lowerCaseName + '.once') || el.getAttribute(':' + lowerCaseName + '.sync') || el.getAttribute('v-bind:' + lowerCaseName + '.sync'));
+        if (value) {
+          warn('Possible usage error for prop `' + lowerCaseName + '` - ' + 'did you mean `' + attr + '`? HTML is case-insensitive, remember to use ' + 'kebab-case for props in templates.', vm);
+        } else if (options.required) {
+          // warn missing required
+          warn('Missing required prop: ' + name, vm);
+        }
+      }
+      // push prop
+      props.push(prop);
+    }
+    return makePropsLinkFn(props);
+  }
+
+  /**
+   * Build a function that applies props to a vm.
+   *
+   * @param {Array} props
+   * @return {Function} propsLinkFn
+   */
+
+  function makePropsLinkFn(props) {
+    return function propsLinkFn(vm, scope) {
+      // store resolved props info
+      vm._props = {};
+      var inlineProps = vm.$options.propsData;
+      var i = props.length;
+      var prop, path, options, value, raw;
+      while (i--) {
+        prop = props[i];
+        raw = prop.raw;
+        path = prop.path;
+        options = prop.options;
+        vm._props[path] = prop;
+        if (inlineProps && hasOwn(inlineProps, path)) {
+          initProp(vm, prop, inlineProps[path]);
+        }if (raw === null) {
+          // initialize absent prop
+          initProp(vm, prop, undefined);
+        } else if (prop.dynamic) {
+          // dynamic prop
+          if (prop.mode === propBindingModes.ONE_TIME) {
+            // one time binding
+            value = (scope || vm._context || vm).$get(prop.parentPath);
+            initProp(vm, prop, value);
+          } else {
+            if (vm._context) {
+              // dynamic binding
+              vm._bindDir({
+                name: 'prop',
+                def: propDef,
+                prop: prop
+              }, null, null, scope); // el, host, scope
+            } else {
+                // root instance
+                initProp(vm, prop, vm.$get(prop.parentPath));
+              }
+          }
+        } else if (prop.optimizedLiteral) {
+          // optimized literal, cast it and just set once
+          var stripped = stripQuotes(raw);
+          value = stripped === raw ? toBoolean(toNumber(raw)) : stripped;
+          initProp(vm, prop, value);
+        } else {
+          // string literal, but we need to cater for
+          // Boolean props with no value, or with same
+          // literal value (e.g. disabled="disabled")
+          // see https://github.com/vuejs/vue-loader/issues/182
+          value = options.type === Boolean && (raw === '' || raw === hyphenate(prop.name)) ? true : raw;
+          initProp(vm, prop, value);
+        }
+      }
+    };
+  }
+
+  /**
+   * Process a prop with a rawValue, applying necessary coersions,
+   * default values & assertions and call the given callback with
+   * processed value.
+   *
+   * @param {Vue} vm
+   * @param {Object} prop
+   * @param {*} rawValue
+   * @param {Function} fn
+   */
+
+  function processPropValue(vm, prop, rawValue, fn) {
+    var isSimple = prop.dynamic && isSimplePath(prop.parentPath);
+    var value = rawValue;
+    if (value === undefined) {
+      value = getPropDefaultValue(vm, prop);
+    }
+    value = coerceProp(prop, value, vm);
+    var coerced = value !== rawValue;
+    if (!assertProp(prop, value, vm)) {
+      value = undefined;
+    }
+    if (isSimple && !coerced) {
+      withoutConversion(function () {
+        fn(value);
+      });
+    } else {
+      fn(value);
+    }
+  }
+
+  /**
+   * Set a prop's initial value on a vm and its data object.
+   *
+   * @param {Vue} vm
+   * @param {Object} prop
+   * @param {*} value
+   */
+
+  function initProp(vm, prop, value) {
+    processPropValue(vm, prop, value, function (value) {
+      defineReactive(vm, prop.path, value);
+    });
+  }
+
+  /**
+   * Update a prop's value on a vm.
+   *
+   * @param {Vue} vm
+   * @param {Object} prop
+   * @param {*} value
+   */
+
+  function updateProp(vm, prop, value) {
+    processPropValue(vm, prop, value, function (value) {
+      vm[prop.path] = value;
+    });
+  }
+
+  /**
+   * Get the default value of a prop.
+   *
+   * @param {Vue} vm
+   * @param {Object} prop
+   * @return {*}
+   */
+
+  function getPropDefaultValue(vm, prop) {
+    // no default, return undefined
+    var options = prop.options;
+    if (!hasOwn(options, 'default')) {
+      // absent boolean value defaults to false
+      return options.type === Boolean ? false : undefined;
+    }
+    var def = options['default'];
+    // warn against non-factory defaults for Object & Array
+    if (isObject(def)) {
+      'development' !== 'production' && warn('Invalid default value for prop "' + prop.name + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm);
+    }
+    // call factory function for non-Function types
+    return typeof def === 'function' && options.type !== Function ? def.call(vm) : def;
+  }
+
+  /**
+   * Assert whether a prop is valid.
+   *
+   * @param {Object} prop
+   * @param {*} value
+   * @param {Vue} vm
+   */
+
+  function assertProp(prop, value, vm) {
+    if (!prop.options.required && ( // non-required
+    prop.raw === null || // abscent
+    value == null) // null or undefined
+    ) {
+        return true;
+      }
+    var options = prop.options;
+    var type = options.type;
+    var valid = !type;
+    var expectedTypes = [];
+    if (type) {
+      if (!isArray(type)) {
+        type = [type];
+      }
+      for (var i = 0; i < type.length && !valid; i++) {
+        var assertedType = assertType(value, type[i]);
+        expectedTypes.push(assertedType.expectedType);
+        valid = assertedType.valid;
+      }
+    }
+    if (!valid) {
+      if ('development' !== 'production') {
+        warn('Invalid prop: type check failed for prop "' + prop.name + '".' + ' Expected ' + expectedTypes.map(formatType).join(', ') + ', got ' + formatValue(value) + '.', vm);
+      }
+      return false;
+    }
+    var validator = options.validator;
+    if (validator) {
+      if (!validator(value)) {
+        'development' !== 'production' && warn('Invalid prop: custom validator check failed for prop "' + prop.name + '".', vm);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Force parsing value with coerce option.
+   *
+   * @param {*} value
+   * @param {Object} options
+   * @return {*}
+   */
+
+  function coerceProp(prop, value, vm) {
+    var coerce = prop.options.coerce;
+    if (!coerce) {
+      return value;
+    }
+    if (typeof coerce === 'function') {
+      return coerce(value);
+    } else {
+      'development' !== 'production' && warn('Invalid coerce for prop "' + prop.name + '": expected function, got ' + typeof coerce + '.', vm);
+      return value;
+    }
+  }
+
+  /**
+   * Assert the type of a value
+   *
+   * @param {*} value
+   * @param {Function} type
+   * @return {Object}
+   */
+
+  function assertType(value, type) {
+    var valid;
+    var expectedType;
+    if (type === String) {
+      expectedType = 'string';
+      valid = typeof value === expectedType;
+    } else if (type === Number) {
+      expectedType = 'number';
+      valid = typeof value === expectedType;
+    } else if (type === Boolean) {
+      expectedType = 'boolean';
+      valid = typeof value === expectedType;
+    } else if (type === Function) {
+      expectedType = 'function';
+      valid = typeof value === expectedType;
+    } else if (type === Object) {
+      expectedType = 'object';
+      valid = isPlainObject(value);
+    } else if (type === Array) {
+      expectedType = 'array';
+      valid = isArray(value);
+    } else {
+      valid = value instanceof type;
+    }
+    return {
+      valid: valid,
+      expectedType: expectedType
+    };
+  }
+
+  /**
+   * Format type for output
+   *
+   * @param {String} type
+   * @return {String}
+   */
+
+  function formatType(type) {
+    return type ? type.charAt(0).toUpperCase() + type.slice(1) : 'custom type';
+  }
+
+  /**
+   * Format value
+   *
+   * @param {*} value
+   * @return {String}
+   */
+
+  function formatValue(val) {
+    return Object.prototype.toString.call(val).slice(8, -1);
+  }
+
+  var bindingModes = config._propBindingModes;
+
+  var propDef = {
+
+    bind: function bind() {
+      var child = this.vm;
+      var parent = child._context;
+      // passed in from compiler directly
+      var prop = this.descriptor.prop;
+      var childKey = prop.path;
+      var parentKey = prop.parentPath;
+      var twoWay = prop.mode === bindingModes.TWO_WAY;
+
+      var parentWatcher = this.parentWatcher = new Watcher(parent, parentKey, function (val) {
+        updateProp(child, prop, val);
+      }, {
+        twoWay: twoWay,
+        filters: prop.filters,
+        // important: props need to be observed on the
+        // v-for scope if present
+        scope: this._scope
+      });
+
+      // set the child initial value.
+      initProp(child, prop, parentWatcher.value);
+
+      // setup two-way binding
+      if (twoWay) {
+        // important: defer the child watcher creation until
+        // the created hook (after data observation)
+        var self = this;
+        child.$once('pre-hook:created', function () {
+          self.childWatcher = new Watcher(child, childKey, function (val) {
+            parentWatcher.set(val);
+          }, {
+            // ensure sync upward before parent sync down.
+            // this is necessary in cases e.g. the child
+            // mutates a prop array, then replaces it. (#1683)
+            sync: true
+          });
+        });
+      }
+    },
+
+    unbind: function unbind() {
+      this.parentWatcher.teardown();
+      if (this.childWatcher) {
+        this.childWatcher.teardown();
+      }
+    }
+  };
+
+  var queue$1 = [];
+  var queued = false;
+
+  /**
+   * Push a job into the queue.
+   *
+   * @param {Function} job
+   */
+
+  function pushJob(job) {
+    queue$1.push(job);
+    if (!queued) {
+      queued = true;
+      nextTick(flush);
+    }
+  }
+
+  /**
+   * Flush the queue, and do one forced reflow before
+   * triggering transitions.
+   */
+
+  function flush() {
+    // Force layout
+    var f = document.documentElement.offsetHeight;
+    for (var i = 0; i < queue$1.length; i++) {
+      queue$1[i]();
+    }
+    queue$1 = [];
+    queued = false;
+    // dummy return, so js linters don't complain about
+    // unused variable f
+    return f;
+  }
+
+  var TYPE_TRANSITION = 'transition';
+  var TYPE_ANIMATION = 'animation';
+  var transDurationProp = transitionProp + 'Duration';
+  var animDurationProp = animationProp + 'Duration';
+
+  /**
+   * If a just-entered element is applied the
+   * leave class while its enter transition hasn't started yet,
+   * and the transitioned property has the same value for both
+   * enter/leave, then the leave transition will be skipped and
+   * the transitionend event never fires. This function ensures
+   * its callback to be called after a transition has started
+   * by waiting for double raf.
+   *
+   * It falls back to setTimeout on devices that support CSS
+   * transitions but not raf (e.g. Android 4.2 browser) - since
+   * these environments are usually slow, we are giving it a
+   * relatively large timeout.
+   */
+
+  var raf = inBrowser && window.requestAnimationFrame;
+  var waitForTransitionStart = raf
+  /* istanbul ignore next */
+  ? function (fn) {
+    raf(function () {
+      raf(fn);
+    });
+  } : function (fn) {
+    setTimeout(fn, 50);
+  };
+
+  /**
+   * A Transition object that encapsulates the state and logic
+   * of the transition.
+   *
+   * @param {Element} el
+   * @param {String} id
+   * @param {Object} hooks
+   * @param {Vue} vm
+   */
+  function Transition(el, id, hooks, vm) {
+    this.id = id;
+    this.el = el;
+    this.enterClass = hooks && hooks.enterClass || id + '-enter';
+    this.leaveClass = hooks && hooks.leaveClass || id + '-leave';
+    this.hooks = hooks;
+    this.vm = vm;
+    // async state
+    this.pendingCssEvent = this.pendingCssCb = this.cancel = this.pendingJsCb = this.op = this.cb = null;
+    this.justEntered = false;
+    this.entered = this.left = false;
+    this.typeCache = {};
+    // check css transition type
+    this.type = hooks && hooks.type;
+    /* istanbul ignore if */
+    if ('development' !== 'production') {
+      if (this.type && this.type !== TYPE_TRANSITION && this.type !== TYPE_ANIMATION) {
+        warn('invalid CSS transition type for transition="' + this.id + '": ' + this.type, vm);
+      }
+    }
+    // bind
+    var self = this;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone'].forEach(function (m) {
+      self[m] = bind(self[m], self);
+    });
+  }
+
+  var p$1 = Transition.prototype;
+
+  /**
+   * Start an entering transition.
+   *
+   * 1. enter transition triggered
+   * 2. call beforeEnter hook
+   * 3. add enter class
+   * 4. insert/show element
+   * 5. call enter hook (with possible explicit js callback)
+   * 6. reflow
+   * 7. based on transition type:
+   *    - transition:
+   *        remove class now, wait for transitionend,
+   *        then done if there's no explicit js callback.
+   *    - animation:
+   *        wait for animationend, remove class,
+   *        then done if there's no explicit js callback.
+   *    - no css transition:
+   *        done now if there's no explicit js callback.
+   * 8. wait for either done or js callback, then call
+   *    afterEnter hook.
+   *
+   * @param {Function} op - insert/show the element
+   * @param {Function} [cb]
+   */
+
+  p$1.enter = function (op, cb) {
+    this.cancelPending();
+    this.callHook('beforeEnter');
+    this.cb = cb;
+    addClass(this.el, this.enterClass);
+    op();
+    this.entered = false;
+    this.callHookWithCb('enter');
+    if (this.entered) {
+      return; // user called done synchronously.
+    }
+    this.cancel = this.hooks && this.hooks.enterCancelled;
+    pushJob(this.enterNextTick);
+  };
+
+  /**
+   * The "nextTick" phase of an entering transition, which is
+   * to be pushed into a queue and executed after a reflow so
+   * that removing the class can trigger a CSS transition.
+   */
+
+  p$1.enterNextTick = function () {
+    var _this = this;
+
+    // prevent transition skipping
+    this.justEntered = true;
+    waitForTransitionStart(function () {
+      _this.justEntered = false;
+    });
+    var enterDone = this.enterDone;
+    var type = this.getCssTransitionType(this.enterClass);
+    if (!this.pendingJsCb) {
+      if (type === TYPE_TRANSITION) {
+        // trigger transition by removing enter class now
+        removeClass(this.el, this.enterClass);
+        this.setupCssCb(transitionEndEvent, enterDone);
+      } else if (type === TYPE_ANIMATION) {
+        this.setupCssCb(animationEndEvent, enterDone);
+      } else {
+        enterDone();
+      }
+    } else if (type === TYPE_TRANSITION) {
+      removeClass(this.el, this.enterClass);
+    }
+  };
+
+  /**
+   * The "cleanup" phase of an entering transition.
+   */
+
+  p$1.enterDone = function () {
+    this.entered = true;
+    this.cancel = this.pendingJsCb = null;
+    removeClass(this.el, this.enterClass);
+    this.callHook('afterEnter');
+    if (this.cb) this.cb();
+  };
+
+  /**
+   * Start a leaving transition.
+   *
+   * 1. leave transition triggered.
+   * 2. call beforeLeave hook
+   * 3. add leave class (trigger css transition)
+   * 4. call leave hook (with possible explicit js callback)
+   * 5. reflow if no explicit js callback is provided
+   * 6. based on transition type:
+   *    - transition or animation:
+   *        wait for end event, remove class, then done if
+   *        there's no explicit js callback.
+   *    - no css transition:
+   *        done if there's no explicit js callback.
+   * 7. wait for either done or js callback, then call
+   *    afterLeave hook.
+   *
+   * @param {Function} op - remove/hide the element
+   * @param {Function} [cb]
+   */
+
+  p$1.leave = function (op, cb) {
+    this.cancelPending();
+    this.callHook('beforeLeave');
+    this.op = op;
+    this.cb = cb;
+    addClass(this.el, this.leaveClass);
+    this.left = false;
+    this.callHookWithCb('leave');
+    if (this.left) {
+      return; // user called done synchronously.
+    }
+    this.cancel = this.hooks && this.hooks.leaveCancelled;
+    // only need to handle leaveDone if
+    // 1. the transition is already done (synchronously called
+    //    by the user, which causes this.op set to null)
+    // 2. there's no explicit js callback
+    if (this.op && !this.pendingJsCb) {
+      // if a CSS transition leaves immediately after enter,
+      // the transitionend event never fires. therefore we
+      // detect such cases and end the leave immediately.
+      if (this.justEntered) {
+        this.leaveDone();
+      } else {
+        pushJob(this.leaveNextTick);
+      }
+    }
+  };
+
+  /**
+   * The "nextTick" phase of a leaving transition.
+   */
+
+  p$1.leaveNextTick = function () {
+    var type = this.getCssTransitionType(this.leaveClass);
+    if (type) {
+      var event = type === TYPE_TRANSITION ? transitionEndEvent : animationEndEvent;
+      this.setupCssCb(event, this.leaveDone);
+    } else {
+      this.leaveDone();
+    }
+  };
+
+  /**
+   * The "cleanup" phase of a leaving transition.
+   */
+
+  p$1.leaveDone = function () {
+    this.left = true;
+    this.cancel = this.pendingJsCb = null;
+    this.op();
+    removeClass(this.el, this.leaveClass);
+    this.callHook('afterLeave');
+    if (this.cb) this.cb();
+    this.op = null;
+  };
+
+  /**
+   * Cancel any pending callbacks from a previously running
+   * but not finished transition.
+   */
+
+  p$1.cancelPending = function () {
+    this.op = this.cb = null;
+    var hasPending = false;
+    if (this.pendingCssCb) {
+      hasPending = true;
+      off(this.el, this.pendingCssEvent, this.pendingCssCb);
+      this.pendingCssEvent = this.pendingCssCb = null;
+    }
+    if (this.pendingJsCb) {
+      hasPending = true;
+      this.pendingJsCb.cancel();
+      this.pendingJsCb = null;
+    }
+    if (hasPending) {
+      removeClass(this.el, this.enterClass);
+      removeClass(this.el, this.leaveClass);
+    }
+    if (this.cancel) {
+      this.cancel.call(this.vm, this.el);
+      this.cancel = null;
+    }
+  };
+
+  /**
+   * Call a user-provided synchronous hook function.
+   *
+   * @param {String} type
+   */
+
+  p$1.callHook = function (type) {
+    if (this.hooks && this.hooks[type]) {
+      this.hooks[type].call(this.vm, this.el);
+    }
+  };
+
+  /**
+   * Call a user-provided, potentially-async hook function.
+   * We check for the length of arguments to see if the hook
+   * expects a `done` callback. If true, the transition's end
+   * will be determined by when the user calls that callback;
+   * otherwise, the end is determined by the CSS transition or
+   * animation.
+   *
+   * @param {String} type
+   */
+
+  p$1.callHookWithCb = function (type) {
+    var hook = this.hooks && this.hooks[type];
+    if (hook) {
+      if (hook.length > 1) {
+        this.pendingJsCb = cancellable(this[type + 'Done']);
+      }
+      hook.call(this.vm, this.el, this.pendingJsCb);
+    }
+  };
+
+  /**
+   * Get an element's transition type based on the
+   * calculated styles.
+   *
+   * @param {String} className
+   * @return {Number}
+   */
+
+  p$1.getCssTransitionType = function (className) {
+    /* istanbul ignore if */
+    if (!transitionEndEvent ||
+    // skip CSS transitions if page is not visible -
+    // this solves the issue of transitionend events not
+    // firing until the page is visible again.
+    // pageVisibility API is supported in IE10+, same as
+    // CSS transitions.
+    document.hidden ||
+    // explicit js-only transition
+    this.hooks && this.hooks.css === false ||
+    // element is hidden
+    isHidden(this.el)) {
+      return;
+    }
+    var type = this.type || this.typeCache[className];
+    if (type) return type;
+    var inlineStyles = this.el.style;
+    var computedStyles = window.getComputedStyle(this.el);
+    var transDuration = inlineStyles[transDurationProp] || computedStyles[transDurationProp];
+    if (transDuration && transDuration !== '0s') {
+      type = TYPE_TRANSITION;
+    } else {
+      var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp];
+      if (animDuration && animDuration !== '0s') {
+        type = TYPE_ANIMATION;
+      }
+    }
+    if (type) {
+      this.typeCache[className] = type;
+    }
+    return type;
+  };
+
+  /**
+   * Setup a CSS transitionend/animationend callback.
+   *
+   * @param {String} event
+   * @param {Function} cb
+   */
+
+  p$1.setupCssCb = function (event, cb) {
+    this.pendingCssEvent = event;
+    var self = this;
+    var el = this.el;
+    var onEnd = this.pendingCssCb = function (e) {
+      if (e.target === el) {
+        off(el, event, onEnd);
+        self.pendingCssEvent = self.pendingCssCb = null;
+        if (!self.pendingJsCb && cb) {
+          cb();
+        }
+      }
+    };
+    on(el, event, onEnd);
+  };
+
+  /**
+   * Check if an element is hidden - in that case we can just
+   * skip the transition alltogether.
+   *
+   * @param {Element} el
+   * @return {Boolean}
+   */
+
+  function isHidden(el) {
+    if (/svg$/.test(el.namespaceURI)) {
+      // SVG elements do not have offset(Width|Height)
+      // so we need to check the client rect
+      var rect = el.getBoundingClientRect();
+      return !(rect.width || rect.height);
+    } else {
+      return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+    }
+  }
+
+  var transition$1 = {
+
+    priority: TRANSITION,
+
+    update: function update(id, oldId) {
+      var el = this.el;
+      // resolve on owner vm
+      var hooks = resolveAsset(this.vm.$options, 'transitions', id);
+      id = id || 'v';
+      oldId = oldId || 'v';
+      el.__v_trans = new Transition(el, id, hooks, this.vm);
+      removeClass(el, oldId + '-transition');
+      addClass(el, id + '-transition');
+    }
+  };
+
+  var internalDirectives = {
+    style: style,
+    'class': vClass,
+    component: component,
+    prop: propDef,
+    transition: transition$1
+  };
+
+  // special binding prefixes
+  var bindRE = /^v-bind:|^:/;
+  var onRE = /^v-on:|^@/;
+  var dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/;
+  var modifierRE = /\.[^\.]+/g;
+  var transitionRE = /^(v-bind:|:)?transition$/;
+
+  // default directive priority
+  var DEFAULT_PRIORITY = 1000;
+  var DEFAULT_TERMINAL_PRIORITY = 2000;
+
+  /**
+   * Compile a template and return a reusable composite link
+   * function, which recursively contains more link functions
+   * inside. This top level compile function would normally
+   * be called on instance root nodes, but can also be used
+   * for partial compilation if the partial argument is true.
+   *
+   * The returned composite link function, when called, will
+   * return an unlink function that tearsdown all directives
+   * created during the linking phase.
+   *
+   * @param {Element|DocumentFragment} el
+   * @param {Object} options
+   * @param {Boolean} partial
+   * @return {Function}
+   */
+
+  function compile(el, options, partial) {
+    // link function for the node itself.
+    var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null;
+    // link function for the childNodes
+    var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && !isScript(el) && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null;
+
+    /**
+     * A composite linker function to be called on a already
+     * compiled piece of DOM, which instantiates all directive
+     * instances.
+     *
+     * @param {Vue} vm
+     * @param {Element|DocumentFragment} el
+     * @param {Vue} [host] - host vm of transcluded content
+     * @param {Object} [scope] - v-for scope
+     * @param {Fragment} [frag] - link context fragment
+     * @return {Function|undefined}
+     */
+
+    return function compositeLinkFn(vm, el, host, scope, frag) {
+      // cache childNodes before linking parent, fix #657
+      var childNodes = toArray(el.childNodes);
+      // link
+      var dirs = linkAndCapture(function compositeLinkCapturer() {
+        if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
+        if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
+      }, vm);
+      return makeUnlinkFn(vm, dirs);
+    };
+  }
+
+  /**
+   * Apply a linker to a vm/element pair and capture the
+   * directives created during the process.
+   *
+   * @param {Function} linker
+   * @param {Vue} vm
+   */
+
+  function linkAndCapture(linker, vm) {
+    /* istanbul ignore if */
+    if ('development' === 'production') {}
+    var originalDirCount = vm._directives.length;
+    linker();
+    var dirs = vm._directives.slice(originalDirCount);
+    dirs.sort(directiveComparator);
+    for (var i = 0, l = dirs.length; i < l; i++) {
+      dirs[i]._bind();
+    }
+    return dirs;
+  }
+
+  /**
+   * Directive priority sort comparator
+   *
+   * @param {Object} a
+   * @param {Object} b
+   */
+
+  function directiveComparator(a, b) {
+    a = a.descriptor.def.priority || DEFAULT_PRIORITY;
+    b = b.descriptor.def.priority || DEFAULT_PRIORITY;
+    return a > b ? -1 : a === b ? 0 : 1;
+  }
+
+  /**
+   * Linker functions return an unlink function that
+   * tearsdown all directives instances generated during
+   * the process.
+   *
+   * We create unlink functions with only the necessary
+   * information to avoid retaining additional closures.
+   *
+   * @param {Vue} vm
+   * @param {Array} dirs
+   * @param {Vue} [context]
+   * @param {Array} [contextDirs]
+   * @return {Function}
+   */
+
+  function makeUnlinkFn(vm, dirs, context, contextDirs) {
+    function unlink(destroying) {
+      teardownDirs(vm, dirs, destroying);
+      if (context && contextDirs) {
+        teardownDirs(context, contextDirs);
+      }
+    }
+    // expose linked directives
+    unlink.dirs = dirs;
+    return unlink;
+  }
+
+  /**
+   * Teardown partial linked directives.
+   *
+   * @param {Vue} vm
+   * @param {Array} dirs
+   * @param {Boolean} destroying
+   */
+
+  function teardownDirs(vm, dirs, destroying) {
+    var i = dirs.length;
+    while (i--) {
+      dirs[i]._teardown();
+      if ('development' !== 'production' && !destroying) {
+        vm._directives.$remove(dirs[i]);
+      }
+    }
+  }
+
+  /**
+   * Compile link props on an instance.
+   *
+   * @param {Vue} vm
+   * @param {Element} el
+   * @param {Object} props
+   * @param {Object} [scope]
+   * @return {Function}
+   */
+
+  function compileAndLinkProps(vm, el, props, scope) {
+    var propsLinkFn = compileProps(el, props, vm);
+    var propDirs = linkAndCapture(function () {
+      propsLinkFn(vm, scope);
+    }, vm);
+    return makeUnlinkFn(vm, propDirs);
+  }
+
+  /**
+   * Compile the root element of an instance.
+   *
+   * 1. attrs on context container (context scope)
+   * 2. attrs on the component template root node, if
+   *    replace:true (child scope)
+   *
+   * If this is a fragment instance, we only need to compile 1.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @param {Object} contextOptions
+   * @return {Function}
+   */
+
+  function compileRoot(el, options, contextOptions) {
+    var containerAttrs = options._containerAttrs;
+    var replacerAttrs = options._replacerAttrs;
+    var contextLinkFn, replacerLinkFn;
+
+    // only need to compile other attributes for
+    // non-fragment instances
+    if (el.nodeType !== 11) {
+      // for components, container and replacer need to be
+      // compiled separately and linked in different scopes.
+      if (options._asComponent) {
+        // 2. container attributes
+        if (containerAttrs && contextOptions) {
+          contextLinkFn = compileDirectives(containerAttrs, contextOptions);
+        }
+        if (replacerAttrs) {
+          // 3. replacer attributes
+          replacerLinkFn = compileDirectives(replacerAttrs, options);
+        }
+      } else {
+        // non-component, just compile as a normal element.
+        replacerLinkFn = compileDirectives(el.attributes, options);
+      }
+    } else if ('development' !== 'production' && containerAttrs) {
+      // warn container directives for fragment instances
+      var names = containerAttrs.filter(function (attr) {
+        // allow vue-loader/vueify scoped css attributes
+        return attr.name.indexOf('_v-') < 0 &&
+        // allow event listeners
+        !onRE.test(attr.name) &&
+        // allow slots
+        attr.name !== 'slot';
+      }).map(function (attr) {
+        return '"' + attr.name + '"';
+      });
+      if (names.length) {
+        var plural = names.length > 1;
+        warn('Attribute' + (plural ? 's ' : ' ') + names.join(', ') + (plural ? ' are' : ' is') + ' ignored on component ' + '<' + options.el.tagName.toLowerCase() + '> because ' + 'the component is a fragment instance: ' + 'http://vuejs.org/guide/components.html#Fragment-Instance');
+      }
+    }
+
+    options._containerAttrs = options._replacerAttrs = null;
+    return function rootLinkFn(vm, el, scope) {
+      // link context scope dirs
+      var context = vm._context;
+      var contextDirs;
+      if (context && contextLinkFn) {
+        contextDirs = linkAndCapture(function () {
+          contextLinkFn(context, el, null, scope);
+        }, context);
+      }
+
+      // link self
+      var selfDirs = linkAndCapture(function () {
+        if (replacerLinkFn) replacerLinkFn(vm, el);
+      }, vm);
+
+      // return the unlink function that tearsdown context
+      // container directives.
+      return makeUnlinkFn(vm, selfDirs, context, contextDirs);
+    };
+  }
+
+  /**
+   * Compile a node and return a nodeLinkFn based on the
+   * node type.
+   *
+   * @param {Node} node
+   * @param {Object} options
+   * @return {Function|null}
+   */
+
+  function compileNode(node, options) {
+    var type = node.nodeType;
+    if (type === 1 && !isScript(node)) {
+      return compileElement(node, options);
+    } else if (type === 3 && node.data.trim()) {
+      return compileTextNode(node, options);
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Compile an element and return a nodeLinkFn.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Function|null}
+   */
+
+  function compileElement(el, options) {
+    // preprocess textareas.
+    // textarea treats its text content as the initial value.
+    // just bind it as an attr directive for value.
+    if (el.tagName === 'TEXTAREA') {
+      var tokens = parseText(el.value);
+      if (tokens) {
+        el.setAttribute(':value', tokensToExp(tokens));
+        el.value = '';
+      }
+    }
+    var linkFn;
+    var hasAttrs = el.hasAttributes();
+    var attrs = hasAttrs && toArray(el.attributes);
+    // check terminal directives (for & if)
+    if (hasAttrs) {
+      linkFn = checkTerminalDirectives(el, attrs, options);
+    }
+    // check element directives
+    if (!linkFn) {
+      linkFn = checkElementDirectives(el, options);
+    }
+    // check component
+    if (!linkFn) {
+      linkFn = checkComponent(el, options);
+    }
+    // normal directives
+    if (!linkFn && hasAttrs) {
+      linkFn = compileDirectives(attrs, options);
+    }
+    return linkFn;
+  }
+
+  /**
+   * Compile a textNode and return a nodeLinkFn.
+   *
+   * @param {TextNode} node
+   * @param {Object} options
+   * @return {Function|null} textNodeLinkFn
+   */
+
+  function compileTextNode(node, options) {
+    // skip marked text nodes
+    if (node._skip) {
+      return removeText;
+    }
+
+    var tokens = parseText(node.wholeText);
+    if (!tokens) {
+      return null;
+    }
+
+    // mark adjacent text nodes as skipped,
+    // because we are using node.wholeText to compile
+    // all adjacent text nodes together. This fixes
+    // issues in IE where sometimes it splits up a single
+    // text node into multiple ones.
+    var next = node.nextSibling;
+    while (next && next.nodeType === 3) {
+      next._skip = true;
+      next = next.nextSibling;
+    }
+
+    var frag = document.createDocumentFragment();
+    var el, token;
+    for (var i = 0, l = tokens.length; i < l; i++) {
+      token = tokens[i];
+      el = token.tag ? processTextToken(token, options) : document.createTextNode(token.value);
+      frag.appendChild(el);
+    }
+    return makeTextNodeLinkFn(tokens, frag, options);
+  }
+
+  /**
+   * Linker for an skipped text node.
+   *
+   * @param {Vue} vm
+   * @param {Text} node
+   */
+
+  function removeText(vm, node) {
+    remove(node);
+  }
+
+  /**
+   * Process a single text token.
+   *
+   * @param {Object} token
+   * @param {Object} options
+   * @return {Node}
+   */
+
+  function processTextToken(token, options) {
+    var el;
+    if (token.oneTime) {
+      el = document.createTextNode(token.value);
+    } else {
+      if (token.html) {
+        el = document.createComment('v-html');
+        setTokenType('html');
+      } else {
+        // IE will clean up empty textNodes during
+        // frag.cloneNode(true), so we have to give it
+        // something here...
+        el = document.createTextNode(' ');
+        setTokenType('text');
+      }
+    }
+    function setTokenType(type) {
+      if (token.descriptor) return;
+      var parsed = parseDirective(token.value);
+      token.descriptor = {
+        name: type,
+        def: directives[type],
+        expression: parsed.expression,
+        filters: parsed.filters
+      };
+    }
+    return el;
+  }
+
+  /**
+   * Build a function that processes a textNode.
+   *
+   * @param {Array<Object>} tokens
+   * @param {DocumentFragment} frag
+   */
+
+  function makeTextNodeLinkFn(tokens, frag) {
+    return function textNodeLinkFn(vm, el, host, scope) {
+      var fragClone = frag.cloneNode(true);
+      var childNodes = toArray(fragClone.childNodes);
+      var token, value, node;
+      for (var i = 0, l = tokens.length; i < l; i++) {
+        token = tokens[i];
+        value = token.value;
+        if (token.tag) {
+          node = childNodes[i];
+          if (token.oneTime) {
+            value = (scope || vm).$eval(value);
+            if (token.html) {
+              replace(node, parseTemplate(value, true));
+            } else {
+              node.data = _toString(value);
+            }
+          } else {
+            vm._bindDir(token.descriptor, node, host, scope);
+          }
+        }
+      }
+      replace(el, fragClone);
+    };
+  }
+
+  /**
+   * Compile a node list and return a childLinkFn.
+   *
+   * @param {NodeList} nodeList
+   * @param {Object} options
+   * @return {Function|undefined}
+   */
+
+  function compileNodeList(nodeList, options) {
+    var linkFns = [];
+    var nodeLinkFn, childLinkFn, node;
+    for (var i = 0, l = nodeList.length; i < l; i++) {
+      node = nodeList[i];
+      nodeLinkFn = compileNode(node, options);
+      childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && node.tagName !== 'SCRIPT' && node.hasChildNodes() ? compileNodeList(node.childNodes, options) : null;
+      linkFns.push(nodeLinkFn, childLinkFn);
+    }
+    return linkFns.length ? makeChildLinkFn(linkFns) : null;
+  }
+
+  /**
+   * Make a child link function for a node's childNodes.
+   *
+   * @param {Array<Function>} linkFns
+   * @return {Function} childLinkFn
+   */
+
+  function makeChildLinkFn(linkFns) {
+    return function childLinkFn(vm, nodes, host, scope, frag) {
+      var node, nodeLinkFn, childrenLinkFn;
+      for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
+        node = nodes[n];
+        nodeLinkFn = linkFns[i++];
+        childrenLinkFn = linkFns[i++];
+        // cache childNodes before linking parent, fix #657
+        var childNodes = toArray(node.childNodes);
+        if (nodeLinkFn) {
+          nodeLinkFn(vm, node, host, scope, frag);
+        }
+        if (childrenLinkFn) {
+          childrenLinkFn(vm, childNodes, host, scope, frag);
+        }
+      }
+    };
+  }
+
+  /**
+   * Check for element directives (custom elements that should
+   * be resovled as terminal directives).
+   *
+   * @param {Element} el
+   * @param {Object} options
+   */
+
+  function checkElementDirectives(el, options) {
+    var tag = el.tagName.toLowerCase();
+    if (commonTagRE.test(tag)) {
+      return;
+    }
+    var def = resolveAsset(options, 'elementDirectives', tag);
+    if (def) {
+      return makeTerminalNodeLinkFn(el, tag, '', options, def);
+    }
+  }
+
+  /**
+   * Check if an element is a component. If yes, return
+   * a component link function.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Function|undefined}
+   */
+
+  function checkComponent(el, options) {
+    var component = checkComponentAttr(el, options);
+    if (component) {
+      var ref = findRef(el);
+      var descriptor = {
+        name: 'component',
+        ref: ref,
+        expression: component.id,
+        def: internalDirectives.component,
+        modifiers: {
+          literal: !component.dynamic
+        }
+      };
+      var componentLinkFn = function componentLinkFn(vm, el, host, scope, frag) {
+        if (ref) {
+          defineReactive((scope || vm).$refs, ref, null);
+        }
+        vm._bindDir(descriptor, el, host, scope, frag);
+      };
+      componentLinkFn.terminal = true;
+      return componentLinkFn;
+    }
+  }
+
+  /**
+   * Check an element for terminal directives in fixed order.
+   * If it finds one, return a terminal link function.
+   *
+   * @param {Element} el
+   * @param {Array} attrs
+   * @param {Object} options
+   * @return {Function} terminalLinkFn
+   */
+
+  function checkTerminalDirectives(el, attrs, options) {
+    // skip v-pre
+    if (getAttr(el, 'v-pre') !== null) {
+      return skip;
+    }
+    // skip v-else block, but only if following v-if
+    if (el.hasAttribute('v-else')) {
+      var prev = el.previousElementSibling;
+      if (prev && prev.hasAttribute('v-if')) {
+        return skip;
+      }
+    }
+
+    var attr, name, value, modifiers, matched, dirName, rawName, arg, def, termDef;
+    for (var i = 0, j = attrs.length; i < j; i++) {
+      attr = attrs[i];
+      name = attr.name.replace(modifierRE, '');
+      if (matched = name.match(dirAttrRE)) {
+        def = resolveAsset(options, 'directives', matched[1]);
+        if (def && def.terminal) {
+          if (!termDef || (def.priority || DEFAULT_TERMINAL_PRIORITY) > termDef.priority) {
+            termDef = def;
+            rawName = attr.name;
+            modifiers = parseModifiers(attr.name);
+            value = attr.value;
+            dirName = matched[1];
+            arg = matched[2];
+          }
+        }
+      }
+    }
+
+    if (termDef) {
+      return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
+    }
+  }
+
+  function skip() {}
+  skip.terminal = true;
+
+  /**
+   * Build a node link function for a terminal directive.
+   * A terminal link function terminates the current
+   * compilation recursion and handles compilation of the
+   * subtree in the directive.
+   *
+   * @param {Element} el
+   * @param {String} dirName
+   * @param {String} value
+   * @param {Object} options
+   * @param {Object} def
+   * @param {String} [rawName]
+   * @param {String} [arg]
+   * @param {Object} [modifiers]
+   * @return {Function} terminalLinkFn
+   */
+
+  function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
+    var parsed = parseDirective(value);
+    var descriptor = {
+      name: dirName,
+      arg: arg,
+      expression: parsed.expression,
+      filters: parsed.filters,
+      raw: value,
+      attr: rawName,
+      modifiers: modifiers,
+      def: def
+    };
+    // check ref for v-for and router-view
+    if (dirName === 'for' || dirName === 'router-view') {
+      descriptor.ref = findRef(el);
+    }
+    var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
+      if (descriptor.ref) {
+        defineReactive((scope || vm).$refs, descriptor.ref, null);
+      }
+      vm._bindDir(descriptor, el, host, scope, frag);
+    };
+    fn.terminal = true;
+    return fn;
+  }
+
+  /**
+   * Compile the directives on an element and return a linker.
+   *
+   * @param {Array|NamedNodeMap} attrs
+   * @param {Object} options
+   * @return {Function}
+   */
+
+  function compileDirectives(attrs, options) {
+    var i = attrs.length;
+    var dirs = [];
+    var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens, matched;
+    while (i--) {
+      attr = attrs[i];
+      name = rawName = attr.name;
+      value = rawValue = attr.value;
+      tokens = parseText(value);
+      // reset arg
+      arg = null;
+      // check modifiers
+      modifiers = parseModifiers(name);
+      name = name.replace(modifierRE, '');
+
+      // attribute interpolations
+      if (tokens) {
+        value = tokensToExp(tokens);
+        arg = name;
+        pushDir('bind', directives.bind, tokens);
+        // warn against mixing mustaches with v-bind
+        if ('development' !== 'production') {
+          if (name === 'class' && Array.prototype.some.call(attrs, function (attr) {
+            return attr.name === ':class' || attr.name === 'v-bind:class';
+          })) {
+            warn('class="' + rawValue + '": Do not mix mustache interpolation ' + 'and v-bind for "class" on the same element. Use one or the other.', options);
+          }
+        }
+      } else
+
+        // special attribute: transition
+        if (transitionRE.test(name)) {
+          modifiers.literal = !bindRE.test(name);
+          pushDir('transition', internalDirectives.transition);
+        } else
+
+          // event handlers
+          if (onRE.test(name)) {
+            arg = name.replace(onRE, '');
+            pushDir('on', directives.on);
+          } else
+
+            // attribute bindings
+            if (bindRE.test(name)) {
+              dirName = name.replace(bindRE, '');
+              if (dirName === 'style' || dirName === 'class') {
+                pushDir(dirName, internalDirectives[dirName]);
+              } else {
+                arg = dirName;
+                pushDir('bind', directives.bind);
+              }
+            } else
+
+              // normal directives
+              if (matched = name.match(dirAttrRE)) {
+                dirName = matched[1];
+                arg = matched[2];
+
+                // skip v-else (when used with v-show)
+                if (dirName === 'else') {
+                  continue;
+                }
+
+                dirDef = resolveAsset(options, 'directives', dirName, true);
+                if (dirDef) {
+                  pushDir(dirName, dirDef);
+                }
+              }
+    }
+
+    /**
+     * Push a directive.
+     *
+     * @param {String} dirName
+     * @param {Object|Function} def
+     * @param {Array} [interpTokens]
+     */
+
+    function pushDir(dirName, def, interpTokens) {
+      var hasOneTimeToken = interpTokens && hasOneTime(interpTokens);
+      var parsed = !hasOneTimeToken && parseDirective(value);
+      dirs.push({
+        name: dirName,
+        attr: rawName,
+        raw: rawValue,
+        def: def,
+        arg: arg,
+        modifiers: modifiers,
+        // conversion from interpolation strings with one-time token
+        // to expression is differed until directive bind time so that we
+        // have access to the actual vm context for one-time bindings.
+        expression: parsed && parsed.expression,
+        filters: parsed && parsed.filters,
+        interp: interpTokens,
+        hasOneTime: hasOneTimeToken
+      });
+    }
+
+    if (dirs.length) {
+      return makeNodeLinkFn(dirs);
+    }
+  }
+
+  /**
+   * Parse modifiers from directive attribute name.
+   *
+   * @param {String} name
+   * @return {Object}
+   */
+
+  function parseModifiers(name) {
+    var res = Object.create(null);
+    var match = name.match(modifierRE);
+    if (match) {
+      var i = match.length;
+      while (i--) {
+        res[match[i].slice(1)] = true;
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Build a link function for all directives on a single node.
+   *
+   * @param {Array} directives
+   * @return {Function} directivesLinkFn
+   */
+
+  function makeNodeLinkFn(directives) {
+    return function nodeLinkFn(vm, el, host, scope, frag) {
+      // reverse apply because it's sorted low to high
+      var i = directives.length;
+      while (i--) {
+        vm._bindDir(directives[i], el, host, scope, frag);
+      }
+    };
+  }
+
+  /**
+   * Check if an interpolation string contains one-time tokens.
+   *
+   * @param {Array} tokens
+   * @return {Boolean}
+   */
+
+  function hasOneTime(tokens) {
+    var i = tokens.length;
+    while (i--) {
+      if (tokens[i].oneTime) return true;
+    }
+  }
+
+  function isScript(el) {
+    return el.tagName === 'SCRIPT' && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript');
+  }
+
+  var specialCharRE = /[^\w\-:\.]/;
+
+  /**
+   * Process an element or a DocumentFragment based on a
+   * instance option object. This allows us to transclude
+   * a template node/fragment before the instance is created,
+   * so the processed fragment can then be cloned and reused
+   * in v-for.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Element|DocumentFragment}
+   */
+
+  function transclude(el, options) {
+    // extract container attributes to pass them down
+    // to compiler, because they need to be compiled in
+    // parent scope. we are mutating the options object here
+    // assuming the same object will be used for compile
+    // right after this.
+    if (options) {
+      options._containerAttrs = extractAttrs(el);
+    }
+    // for template tags, what we want is its content as
+    // a documentFragment (for fragment instances)
+    if (isTemplate(el)) {
+      el = parseTemplate(el);
+    }
+    if (options) {
+      if (options._asComponent && !options.template) {
+        options.template = '<slot></slot>';
+      }
+      if (options.template) {
+        options._content = extractContent(el);
+        el = transcludeTemplate(el, options);
+      }
+    }
+    if (isFragment(el)) {
+      // anchors for fragment instance
+      // passing in `persist: true` to avoid them being
+      // discarded by IE during template cloning
+      prepend(createAnchor('v-start', true), el);
+      el.appendChild(createAnchor('v-end', true));
+    }
+    return el;
+  }
+
+  /**
+   * Process the template option.
+   * If the replace option is true this will swap the $el.
+   *
+   * @param {Element} el
+   * @param {Object} options
+   * @return {Element|DocumentFragment}
+   */
+
+  function transcludeTemplate(el, options) {
+    var template = options.template;
+    var frag = parseTemplate(template, true);
+    if (frag) {
+      var replacer = frag.firstChild;
+      var tag = replacer.tagName && replacer.tagName.toLowerCase();
+      if (options.replace) {
+        /* istanbul ignore if */
+        if (el === document.body) {
+          'development' !== 'production' && warn('You are mounting an instance with a template to ' + '<body>. This will replace <body> entirely. You ' + 'should probably use `replace: false` here.');
+        }
+        // there are many cases where the instance must
+        // become a fragment instance: basically anything that
+        // can create more than 1 root nodes.
+        if (
+        // multi-children template
+        frag.childNodes.length > 1 ||
+        // non-element template
+        replacer.nodeType !== 1 ||
+        // single nested component
+        tag === 'component' || resolveAsset(options, 'components', tag) || hasBindAttr(replacer, 'is') ||
+        // element directive
+        resolveAsset(options, 'elementDirectives', tag) ||
+        // for block
+        replacer.hasAttribute('v-for') ||
+        // if block
+        replacer.hasAttribute('v-if')) {
+          return frag;
+        } else {
+          options._replacerAttrs = extractAttrs(replacer);
+          mergeAttrs(el, replacer);
+          return replacer;
+        }
+      } else {
+        el.appendChild(frag);
+        return el;
+      }
+    } else {
+      'development' !== 'production' && warn('Invalid template option: ' + template);
+    }
+  }
+
+  /**
+   * Helper to extract a component container's attributes
+   * into a plain object array.
+   *
+   * @param {Element} el
+   * @return {Array}
+   */
+
+  function extractAttrs(el) {
+    if (el.nodeType === 1 && el.hasAttributes()) {
+      return toArray(el.attributes);
+    }
+  }
+
+  /**
+   * Merge the attributes of two elements, and make sure
+   * the class names are merged properly.
+   *
+   * @param {Element} from
+   * @param {Element} to
+   */
+
+  function mergeAttrs(from, to) {
+    var attrs = from.attributes;
+    var i = attrs.length;
+    var name, value;
+    while (i--) {
+      name = attrs[i].name;
+      value = attrs[i].value;
+      if (!to.hasAttribute(name) && !specialCharRE.test(name)) {
+        to.setAttribute(name, value);
+      } else if (name === 'class' && !parseText(value) && (value = value.trim())) {
+        value.split(/\s+/).forEach(function (cls) {
+          addClass(to, cls);
+        });
+      }
+    }
+  }
+
+  /**
+   * Scan and determine slot content distribution.
+   * We do this during transclusion instead at compile time so that
+   * the distribution is decoupled from the compilation order of
+   * the slots.
+   *
+   * @param {Element|DocumentFragment} template
+   * @param {Element} content
+   * @param {Vue} vm
+   */
+
+  function resolveSlots(vm, content) {
+    if (!content) {
+      return;
+    }
+    var contents = vm._slotContents = Object.create(null);
+    var el, name;
+    for (var i = 0, l = content.children.length; i < l; i++) {
+      el = content.children[i];
+      /* eslint-disable no-cond-assign */
+      if (name = el.getAttribute('slot')) {
+        (contents[name] || (contents[name] = [])).push(el);
+      }
+      /* eslint-enable no-cond-assign */
+      if ('development' !== 'production' && getBindAttr(el, 'slot')) {
+        warn('The "slot" attribute must be static.', vm.$parent);
+      }
+    }
+    for (name in contents) {
+      contents[name] = extractFragment(contents[name], content);
+    }
+    if (content.hasChildNodes()) {
+      var nodes = content.childNodes;
+      if (nodes.length === 1 && nodes[0].nodeType === 3 && !nodes[0].data.trim()) {
+        return;
+      }
+      contents['default'] = extractFragment(content.childNodes, content);
+    }
+  }
+
+  /**
+   * Extract qualified content nodes from a node list.
+   *
+   * @param {NodeList} nodes
+   * @return {DocumentFragment}
+   */
+
+  function extractFragment(nodes, parent) {
+    var frag = document.createDocumentFragment();
+    nodes = toArray(nodes);
+    for (var i = 0, l = nodes.length; i < l; i++) {
+      var node = nodes[i];
+      if (isTemplate(node) && !node.hasAttribute('v-if') && !node.hasAttribute('v-for')) {
+        parent.removeChild(node);
+        node = parseTemplate(node, true);
+      }
+      frag.appendChild(node);
+    }
+    return frag;
+  }
+
+
+
+  var compiler = Object.freeze({
+  	compile: compile,
+  	compileAndLinkProps: compileAndLinkProps,
+  	compileRoot: compileRoot,
+  	transclude: transclude,
+  	resolveSlots: resolveSlots
+  });
+
+  function stateMixin (Vue) {
+    /**
+     * Accessor for `$data` property, since setting $data
+     * requires observing the new object and updating
+     * proxied properties.
+     */
+
+    Object.defineProperty(Vue.prototype, '$data', {
+      get: function get() {
+        return this._data;
+      },
+      set: function set(newData) {
+        if (newData !== this._data) {
+          this._setData(newData);
+        }
+      }
+    });
+
+    /**
+     * Setup the scope of an instance, which contains:
+     * - observed data
+     * - computed properties
+     * - user methods
+     * - meta properties
+     */
+
+    Vue.prototype._initState = function () {
+      this._initProps();
+      this._initMeta();
+      this._initMethods();
+      this._initData();
+      this._initComputed();
+    };
+
+    /**
+     * Initialize props.
+     */
+
+    Vue.prototype._initProps = function () {
+      var options = this.$options;
+      var el = options.el;
+      var props = options.props;
+      if (props && !el) {
+        'development' !== 'production' && warn('Props will not be compiled if no `el` option is ' + 'provided at instantiation.', this);
+      }
+      // make sure to convert string selectors into element now
+      el = options.el = query(el);
+      this._propsUnlinkFn = el && el.nodeType === 1 && props
+      // props must be linked in proper scope if inside v-for
+      ? compileAndLinkProps(this, el, props, this._scope) : null;
+    };
+
+    /**
+     * Initialize the data.
+     */
+
+    Vue.prototype._initData = function () {
+      var dataFn = this.$options.data;
+      var data = this._data = dataFn ? dataFn() : {};
+      if (!isPlainObject(data)) {
+        data = {};
+        'development' !== 'production' && warn('data functions should return an object.', this);
+      }
+      var props = this._props;
+      // proxy data on instance
+      var keys = Object.keys(data);
+      var i, key;
+      i = keys.length;
+      while (i--) {
+        key = keys[i];
+        // there are two scenarios where we can proxy a data key:
+        // 1. it's not already defined as a prop
+        // 2. it's provided via a instantiation option AND there are no
+        //    template prop present
+        if (!props || !hasOwn(props, key)) {
+          this._proxy(key);
+        } else if ('development' !== 'production') {
+          warn('Data field "' + key + '" is already defined ' + 'as a prop. To provide default value for a prop, use the "default" ' + 'prop option; if you want to pass prop values to an instantiation ' + 'call, use the "propsData" option.', this);
+        }
+      }
+      // observe data
+      observe(data, this);
+    };
+
+    /**
+     * Swap the instance's $data. Called in $data's setter.
+     *
+     * @param {Object} newData
+     */
+
+    Vue.prototype._setData = function (newData) {
+      newData = newData || {};
+      var oldData = this._data;
+      this._data = newData;
+      var keys, key, i;
+      // unproxy keys not present in new data
+      keys = Object.keys(oldData);
+      i = keys.length;
+      while (i--) {
+        key = keys[i];
+        if (!(key in newData)) {
+          this._unproxy(key);
+        }
+      }
+      // proxy keys not already proxied,
+      // and trigger change for changed values
+      keys = Object.keys(newData);
+      i = keys.length;
+      while (i--) {
+        key = keys[i];
+        if (!hasOwn(this, key)) {
+          // new property
+          this._proxy(key);
+        }
+      }
+      oldData.__ob__.removeVm(this);
+      observe(newData, this);
+      this._digest();
+    };
+
+    /**
+     * Proxy a property, so that
+     * vm.prop === vm._data.prop
+     *
+     * @param {String} key
+     */
+
+    Vue.prototype._proxy = function (key) {
+      if (!isReserved(key)) {
+        // need to store ref to self here
+        // because these getter/setters might
+        // be called by child scopes via
+        // prototype inheritance.
+        var self = this;
+        Object.defineProperty(self, key, {
+          configurable: true,
+          enumerable: true,
+          get: function proxyGetter() {
+            return self._data[key];
+          },
+          set: function proxySetter(val) {
+            self._data[key] = val;
+          }
+        });
+      }
+    };
+
+    /**
+     * Unproxy a property.
+     *
+     * @param {String} key
+     */
+
+    Vue.prototype._unproxy = function (key) {
+      if (!isReserved(key)) {
+        delete this[key];
+      }
+    };
+
+    /**
+     * Force update on every watcher in scope.
+     */
+
+    Vue.prototype._digest = function () {
+      for (var i = 0, l = this._watchers.length; i < l; i++) {
+        this._watchers[i].update(true); // shallow updates
+      }
+    };
+
+    /**
+     * Setup computed properties. They are essentially
+     * special getter/setters
+     */
+
+    function noop() {}
+    Vue.prototype._initComputed = function () {
+      var computed = this.$options.computed;
+      if (computed) {
+        for (var key in computed) {
+          var userDef = computed[key];
+          var def = {
+            enumerable: true,
+            configurable: true
+          };
+          if (typeof userDef === 'function') {
+            def.get = makeComputedGetter(userDef, this);
+            def.set = noop;
+          } else {
+            def.get = userDef.get ? userDef.cache !== false ? makeComputedGetter(userDef.get, this) : bind(userDef.get, this) : noop;
+            def.set = userDef.set ? bind(userDef.set, this) : noop;
+          }
+          Object.defineProperty(this, key, def);
+        }
+      }
+    };
+
+    function makeComputedGetter(getter, owner) {
+      var watcher = new Watcher(owner, getter, null, {
+        lazy: true
+      });
+      return function computedGetter() {
+        if (watcher.dirty) {
+          watcher.evaluate();
+        }
+        if (Dep.target) {
+          watcher.depend();
+        }
+        return watcher.value;
+      };
+    }
+
+    /**
+     * Setup instance methods. Methods must be bound to the
+     * instance since they might be passed down as a prop to
+     * child components.
+     */
+
+    Vue.prototype._initMethods = function () {
+      var methods = this.$options.methods;
+      if (methods) {
+        for (var key in methods) {
+          this[key] = bind(methods[key], this);
+        }
+      }
+    };
+
+    /**
+     * Initialize meta information like $index, $key & $value.
+     */
+
+    Vue.prototype._initMeta = function () {
+      var metas = this.$options._meta;
+      if (metas) {
+        for (var key in metas) {
+          defineReactive(this, key, metas[key]);
+        }
+      }
+    };
+  }
+
+  var eventRE = /^v-on:|^@/;
+
+  function eventsMixin (Vue) {
+    /**
+     * Setup the instance's option events & watchers.
+     * If the value is a string, we pull it from the
+     * instance's methods by name.
+     */
+
+    Vue.prototype._initEvents = function () {
+      var options = this.$options;
+      if (options._asComponent) {
+        registerComponentEvents(this, options.el);
+      }
+      registerCallbacks(this, '$on', options.events);
+      registerCallbacks(this, '$watch', options.watch);
+    };
+
+    /**
+     * Register v-on events on a child component
+     *
+     * @param {Vue} vm
+     * @param {Element} el
+     */
+
+    function registerComponentEvents(vm, el) {
+      var attrs = el.attributes;
+      var name, value, handler;
+      for (var i = 0, l = attrs.length; i < l; i++) {
+        name = attrs[i].name;
+        if (eventRE.test(name)) {
+          name = name.replace(eventRE, '');
+          // force the expression into a statement so that
+          // it always dynamically resolves the method to call (#2670)
+          // kinda ugly hack, but does the job.
+          value = attrs[i].value;
+          if (isSimplePath(value)) {
+            value += '.apply(this, $arguments)';
+          }
+          handler = (vm._scope || vm._context).$eval(value, true);
+          handler._fromParent = true;
+          vm.$on(name.replace(eventRE), handler);
+        }
+      }
+    }
+
+    /**
+     * Register callbacks for option events and watchers.
+     *
+     * @param {Vue} vm
+     * @param {String} action
+     * @param {Object} hash
+     */
+
+    function registerCallbacks(vm, action, hash) {
+      if (!hash) return;
+      var handlers, key, i, j;
+      for (key in hash) {
+        handlers = hash[key];
+        if (isArray(handlers)) {
+          for (i = 0, j = handlers.length; i < j; i++) {
+            register(vm, action, key, handlers[i]);
+          }
+        } else {
+          register(vm, action, key, handlers);
+        }
+      }
+    }
+
+    /**
+     * Helper to register an event/watch callback.
+     *
+     * @param {Vue} vm
+     * @param {String} action
+     * @param {String} key
+     * @param {Function|String|Object} handler
+     * @param {Object} [options]
+     */
+
+    function register(vm, action, key, handler, options) {
+      var type = typeof handler;
+      if (type === 'function') {
+        vm[action](key, handler, options);
+      } else if (type === 'string') {
+        var methods = vm.$options.methods;
+        var method = methods && methods[handler];
+        if (method) {
+          vm[action](key, method, options);
+        } else {
+          'development' !== 'production' && warn('Unknown method: "' + handler + '" when ' + 'registering callback for ' + action + ': "' + key + '".', vm);
+        }
+      } else if (handler && type === 'object') {
+        register(vm, action, key, handler.handler, handler);
+      }
+    }
+
+    /**
+     * Setup recursive attached/detached calls
+     */
+
+    Vue.prototype._initDOMHooks = function () {
+      this.$on('hook:attached', onAttached);
+      this.$on('hook:detached', onDetached);
+    };
+
+    /**
+     * Callback to recursively call attached hook on children
+     */
+
+    function onAttached() {
+      if (!this._isAttached) {
+        this._isAttached = true;
+        this.$children.forEach(callAttach);
+      }
+    }
+
+    /**
+     * Iterator to call attached hook
+     *
+     * @param {Vue} child
+     */
+
+    function callAttach(child) {
+      if (!child._isAttached && inDoc(child.$el)) {
+        child._callHook('attached');
+      }
+    }
+
+    /**
+     * Callback to recursively call detached hook on children
+     */
+
+    function onDetached() {
+      if (this._isAttached) {
+        this._isAttached = false;
+        this.$children.forEach(callDetach);
+      }
+    }
+
+    /**
+     * Iterator to call detached hook
+     *
+     * @param {Vue} child
+     */
+
+    function callDetach(child) {
+      if (child._isAttached && !inDoc(child.$el)) {
+        child._callHook('detached');
+      }
+    }
+
+    /**
+     * Trigger all handlers for a hook
+     *
+     * @param {String} hook
+     */
+
+    Vue.prototype._callHook = function (hook) {
+      this.$emit('pre-hook:' + hook);
+      var handlers = this.$options[hook];
+      if (handlers) {
+        for (var i = 0, j = handlers.length; i < j; i++) {
+          handlers[i].call(this);
+        }
+      }
+      this.$emit('hook:' + hook);
+    };
+  }
+
+  function noop$1() {}
+
+  /**
+   * A directive links a DOM element with a piece of data,
+   * which is the result of evaluating an expression.
+   * It registers a watcher with the expression and calls
+   * the DOM update function when a change is triggered.
+   *
+   * @param {Object} descriptor
+   *                 - {String} name
+   *                 - {Object} def
+   *                 - {String} expression
+   *                 - {Array<Object>} [filters]
+   *                 - {Object} [modifiers]
+   *                 - {Boolean} literal
+   *                 - {String} attr
+   *                 - {String} arg
+   *                 - {String} raw
+   *                 - {String} [ref]
+   *                 - {Array<Object>} [interp]
+   *                 - {Boolean} [hasOneTime]
+   * @param {Vue} vm
+   * @param {Node} el
+   * @param {Vue} [host] - transclusion host component
+   * @param {Object} [scope] - v-for scope
+   * @param {Fragment} [frag] - owner fragment
+   * @constructor
+   */
+  function Directive(descriptor, vm, el, host, scope, frag) {
+    this.vm = vm;
+    this.el = el;
+    // copy descriptor properties
+    this.descriptor = descriptor;
+    this.name = descriptor.name;
+    this.expression = descriptor.expression;
+    this.arg = descriptor.arg;
+    this.modifiers = descriptor.modifiers;
+    this.filters = descriptor.filters;
+    this.literal = this.modifiers && this.modifiers.literal;
+    // private
+    this._locked = false;
+    this._bound = false;
+    this._listeners = null;
+    // link context
+    this._host = host;
+    this._scope = scope;
+    this._frag = frag;
+    // store directives on node in dev mode
+    if ('development' !== 'production' && this.el) {
+      this.el._vue_directives = this.el._vue_directives || [];
+      this.el._vue_directives.push(this);
+    }
+  }
+
+  /**
+   * Initialize the directive, mixin definition properties,
+   * setup the watcher, call definition bind() and update()
+   * if present.
+   */
+
+  Directive.prototype._bind = function () {
+    var name = this.name;
+    var descriptor = this.descriptor;
+
+    // remove attribute
+    if ((name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute) {
+      var attr = descriptor.attr || 'v-' + name;
+      this.el.removeAttribute(attr);
+    }
+
+    // copy def properties
+    var def = descriptor.def;
+    if (typeof def === 'function') {
+      this.update = def;
+    } else {
+      extend(this, def);
+    }
+
+    // setup directive params
+    this._setupParams();
+
+    // initial bind
+    if (this.bind) {
+      this.bind();
+    }
+    this._bound = true;
+
+    if (this.literal) {
+      this.update && this.update(descriptor.raw);
+    } else if ((this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement()) {
+      // wrapped updater for context
+      var dir = this;
+      if (this.update) {
+        this._update = function (val, oldVal) {
+          if (!dir._locked) {
+            dir.update(val, oldVal);
+          }
+        };
+      } else {
+        this._update = noop$1;
+      }
+      var preProcess = this._preProcess ? bind(this._preProcess, this) : null;
+      var postProcess = this._postProcess ? bind(this._postProcess, this) : null;
+      var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // callback
+      {
+        filters: this.filters,
+        twoWay: this.twoWay,
+        deep: this.deep,
+        preProcess: preProcess,
+        postProcess: postProcess,
+        scope: this._scope
+      });
+      // v-model with inital inline value need to sync back to
+      // model instead of update to DOM on init. They would
+      // set the afterBind hook to indicate that.
+      if (this.afterBind) {
+        this.afterBind();
+      } else if (this.update) {
+        this.update(watcher.value);
+      }
+    }
+  };
+
+  /**
+   * Setup all param attributes, e.g. track-by,
+   * transition-mode, etc...
+   */
+
+  Directive.prototype._setupParams = function () {
+    if (!this.params) {
+      return;
+    }
+    var params = this.params;
+    // swap the params array with a fresh object.
+    this.params = Object.create(null);
+    var i = params.length;
+    var key, val, mappedKey;
+    while (i--) {
+      key = hyphenate(params[i]);
+      mappedKey = camelize(key);
+      val = getBindAttr(this.el, key);
+      if (val != null) {
+        // dynamic
+        this._setupParamWatcher(mappedKey, val);
+      } else {
+        // static
+        val = getAttr(this.el, key);
+        if (val != null) {
+          this.params[mappedKey] = val === '' ? true : val;
+        }
+      }
+    }
+  };
+
+  /**
+   * Setup a watcher for a dynamic param.
+   *
+   * @param {String} key
+   * @param {String} expression
+   */
+
+  Directive.prototype._setupParamWatcher = function (key, expression) {
+    var self = this;
+    var called = false;
+    var unwatch = (this._scope || this.vm).$watch(expression, function (val, oldVal) {
+      self.params[key] = val;
+      // since we are in immediate mode,
+      // only call the param change callbacks if this is not the first update.
+      if (called) {
+        var cb = self.paramWatchers && self.paramWatchers[key];
+        if (cb) {
+          cb.call(self, val, oldVal);
+        }
+      } else {
+        called = true;
+      }
+    }, {
+      immediate: true,
+      user: false
+    });(this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch);
+  };
+
+  /**
+   * Check if the directive is a function caller
+   * and if the expression is a callable one. If both true,
+   * we wrap up the expression and use it as the event
+   * handler.
+   *
+   * e.g. on-click="a++"
+   *
+   * @return {Boolean}
+   */
+
+  Directive.prototype._checkStatement = function () {
+    var expression = this.expression;
+    if (expression && this.acceptStatement && !isSimplePath(expression)) {
+      var fn = parseExpression(expression).get;
+      var scope = this._scope || this.vm;
+      var handler = function handler(e) {
+        scope.$event = e;
+        fn.call(scope, scope);
+        scope.$event = null;
+      };
+      if (this.filters) {
+        handler = scope._applyFilters(handler, null, this.filters);
+      }
+      this.update(handler);
+      return true;
+    }
+  };
+
+  /**
+   * Set the corresponding value with the setter.
+   * This should only be used in two-way directives
+   * e.g. v-model.
+   *
+   * @param {*} value
+   * @public
+   */
+
+  Directive.prototype.set = function (value) {
+    /* istanbul ignore else */
+    if (this.twoWay) {
+      this._withLock(function () {
+        this._watcher.set(value);
+      });
+    } else if ('development' !== 'production') {
+      warn('Directive.set() can only be used inside twoWay' + 'directives.');
+    }
+  };
+
+  /**
+   * Execute a function while preventing that function from
+   * triggering updates on this directive instance.
+   *
+   * @param {Function} fn
+   */
+
+  Directive.prototype._withLock = function (fn) {
+    var self = this;
+    self._locked = true;
+    fn.call(self);
+    nextTick(function () {
+      self._locked = false;
+    });
+  };
+
+  /**
+   * Convenience method that attaches a DOM event listener
+   * to the directive element and autometically tears it down
+   * during unbind.
+   *
+   * @param {String} event
+   * @param {Function} handler
+   * @param {Boolean} [useCapture]
+   */
+
+  Directive.prototype.on = function (event, handler, useCapture) {
+    on(this.el, event, handler, useCapture);(this._listeners || (this._listeners = [])).push([event, handler]);
+  };
+
+  /**
+   * Teardown the watcher and call unbind.
+   */
+
+  Directive.prototype._teardown = function () {
+    if (this._bound) {
+      this._bound = false;
+      if (this.unbind) {
+        this.unbind();
+      }
+      if (this._watcher) {
+        this._watcher.teardown();
+      }
+      var listeners = this._listeners;
+      var i;
+      if (listeners) {
+        i = listeners.length;
+        while (i--) {
+          off(this.el, listeners[i][0], listeners[i][1]);
+        }
+      }
+      var unwatchFns = this._paramUnwatchFns;
+      if (unwatchFns) {
+        i = unwatchFns.length;
+        while (i--) {
+          unwatchFns[i]();
+        }
+      }
+      if ('development' !== 'production' && this.el) {
+        this.el._vue_directives.$remove(this);
+      }
+      this.vm = this.el = this._watcher = this._listeners = null;
+    }
+  };
+
+  function lifecycleMixin (Vue) {
+    /**
+     * Update v-ref for component.
+     *
+     * @param {Boolean} remove
+     */
+
+    Vue.prototype._updateRef = function (remove) {
+      var ref = this.$options._ref;
+      if (ref) {
+        var refs = (this._scope || this._context).$refs;
+        if (remove) {
+          if (refs[ref] === this) {
+            refs[ref] = null;
+          }
+        } else {
+          refs[ref] = this;
+        }
+      }
+    };
+
+    /**
+     * Transclude, compile and link element.
+     *
+     * If a pre-compiled linker is available, that means the
+     * passed in element will be pre-transcluded and compiled
+     * as well - all we need to do is to call the linker.
+     *
+     * Otherwise we need to call transclude/compile/link here.
+     *
+     * @param {Element} el
+     */
+
+    Vue.prototype._compile = function (el) {
+      var options = this.$options;
+
+      // transclude and init element
+      // transclude can potentially replace original
+      // so we need to keep reference; this step also injects
+      // the template and caches the original attributes
+      // on the container node and replacer node.
+      var original = el;
+      el = transclude(el, options);
+      this._initElement(el);
+
+      // handle v-pre on root node (#2026)
+      if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {
+        return;
+      }
+
+      // root is always compiled per-instance, because
+      // container attrs and props can be different every time.
+      var contextOptions = this._context && this._context.$options;
+      var rootLinker = compileRoot(el, options, contextOptions);
+
+      // resolve slot distribution
+      resolveSlots(this, options._content);
+
+      // compile and link the rest
+      var contentLinkFn;
+      var ctor = this.constructor;
+      // component compilation can be cached
+      // as long as it's not using inline-template
+      if (options._linkerCachable) {
+        contentLinkFn = ctor.linker;
+        if (!contentLinkFn) {
+          contentLinkFn = ctor.linker = compile(el, options);
+        }
+      }
+
+      // link phase
+      // make sure to link root with prop scope!
+      var rootUnlinkFn = rootLinker(this, el, this._scope);
+      var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el);
+
+      // register composite unlink function
+      // to be called during instance destruction
+      this._unlinkFn = function () {
+        rootUnlinkFn();
+        // passing destroying: true to avoid searching and
+        // splicing the directives
+        contentUnlinkFn(true);
+      };
+
+      // finally replace original
+      if (options.replace) {
+        replace(original, el);
+      }
+
+      this._isCompiled = true;
+      this._callHook('compiled');
+    };
+
+    /**
+     * Initialize instance element. Called in the public
+     * $mount() method.
+     *
+     * @param {Element} el
+     */
+
+    Vue.prototype._initElement = function (el) {
+      if (isFragment(el)) {
+        this._isFragment = true;
+        this.$el = this._fragmentStart = el.firstChild;
+        this._fragmentEnd = el.lastChild;
+        // set persisted text anchors to empty
+        if (this._fragmentStart.nodeType === 3) {
+          this._fragmentStart.data = this._fragmentEnd.data = '';
+        }
+        this._fragment = el;
+      } else {
+        this.$el = el;
+      }
+      this.$el.__vue__ = this;
+      this._callHook('beforeCompile');
+    };
+
+    /**
+     * Create and bind a directive to an element.
+     *
+     * @param {Object} descriptor - parsed directive descriptor
+     * @param {Node} node   - target node
+     * @param {Vue} [host] - transclusion host component
+     * @param {Object} [scope] - v-for scope
+     * @param {Fragment} [frag] - owner fragment
+     */
+
+    Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
+      this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
+    };
+
+    /**
+     * Teardown an instance, unobserves the data, unbind all the
+     * directives, turn off all the event listeners, etc.
+     *
+     * @param {Boolean} remove - whether to remove the DOM node.
+     * @param {Boolean} deferCleanup - if true, defer cleanup to
+     *                                 be called later
+     */
+
+    Vue.prototype._destroy = function (remove, deferCleanup) {
+      if (this._isBeingDestroyed) {
+        if (!deferCleanup) {
+          this._cleanup();
+        }
+        return;
+      }
+
+      var destroyReady;
+      var pendingRemoval;
+
+      var self = this;
+      // Cleanup should be called either synchronously or asynchronoysly as
+      // callback of this.$remove(), or if remove and deferCleanup are false.
+      // In any case it should be called after all other removing, unbinding and
+      // turning of is done
+      var cleanupIfPossible = function cleanupIfPossible() {
+        if (destroyReady && !pendingRemoval && !deferCleanup) {
+          self._cleanup();
+        }
+      };
+
+      // remove DOM element
+      if (remove && this.$el) {
+        pendingRemoval = true;
+        this.$remove(function () {
+          pendingRemoval = false;
+          cleanupIfPossible();
+        });
+      }
+
+      this._callHook('beforeDestroy');
+      this._isBeingDestroyed = true;
+      var i;
+      // remove self from parent. only necessary
+      // if parent is not being destroyed as well.
+      var parent = this.$parent;
+      if (parent && !parent._isBeingDestroyed) {
+        parent.$children.$remove(this);
+        // unregister ref (remove: true)
+        this._updateRef(true);
+      }
+      // destroy all children.
+      i = this.$children.length;
+      while (i--) {
+        this.$children[i].$destroy();
+      }
+      // teardown props
+      if (this._propsUnlinkFn) {
+        this._propsUnlinkFn();
+      }
+      // teardown all directives. this also tearsdown all
+      // directive-owned watchers.
+      if (this._unlinkFn) {
+        this._unlinkFn();
+      }
+      i = this._watchers.length;
+      while (i--) {
+        this._watchers[i].teardown();
+      }
+      // remove reference to self on $el
+      if (this.$el) {
+        this.$el.__vue__ = null;
+      }
+
+      destroyReady = true;
+      cleanupIfPossible();
+    };
+
+    /**
+     * Clean up to ensure garbage collection.
+     * This is called after the leave transition if there
+     * is any.
+     */
+
+    Vue.prototype._cleanup = function () {
+      if (this._isDestroyed) {
+        return;
+      }
+      // remove self from owner fragment
+      // do it in cleanup so that we can call $destroy with
+      // defer right when a fragment is about to be removed.
+      if (this._frag) {
+        this._frag.children.$remove(this);
+      }
+      // remove reference from data ob
+      // frozen object may not have observer.
+      if (this._data && this._data.__ob__) {
+        this._data.__ob__.removeVm(this);
+      }
+      // Clean up references to private properties and other
+      // instances. preserve reference to _data so that proxy
+      // accessors still work. The only potential side effect
+      // here is that mutating the instance after it's destroyed
+      // may affect the state of other components that are still
+      // observing the same object, but that seems to be a
+      // reasonable responsibility for the user rather than
+      // always throwing an error on them.
+      this.$el = this.$parent = this.$root = this.$children = this._watchers = this._context = this._scope = this._directives = null;
+      // call the last hook...
+      this._isDestroyed = true;
+      this._callHook('destroyed');
+      // turn off all instance listeners.
+      this.$off();
+    };
+  }
+
+  function miscMixin (Vue) {
+    /**
+     * Apply a list of filter (descriptors) to a value.
+     * Using plain for loops here because this will be called in
+     * the getter of any watcher with filters so it is very
+     * performance sensitive.
+     *
+     * @param {*} value
+     * @param {*} [oldValue]
+     * @param {Array} filters
+     * @param {Boolean} write
+     * @return {*}
+     */
+
+    Vue.prototype._applyFilters = function (value, oldValue, filters, write) {
+      var filter, fn, args, arg, offset, i, l, j, k;
+      for (i = 0, l = filters.length; i < l; i++) {
+        filter = filters[write ? l - i - 1 : i];
+        fn = resolveAsset(this.$options, 'filters', filter.name, true);
+        if (!fn) continue;
+        fn = write ? fn.write : fn.read || fn;
+        if (typeof fn !== 'function') continue;
+        args = write ? [value, oldValue] : [value];
+        offset = write ? 2 : 1;
+        if (filter.args) {
+          for (j = 0, k = filter.args.length; j < k; j++) {
+            arg = filter.args[j];
+            args[j + offset] = arg.dynamic ? this.$get(arg.value) : arg.value;
+          }
+        }
+        value = fn.apply(this, args);
+      }
+      return value;
+    };
+
+    /**
+     * Resolve a component, depending on whether the component
+     * is defined normally or using an async factory function.
+     * Resolves synchronously if already resolved, otherwise
+     * resolves asynchronously and caches the resolved
+     * constructor on the factory.
+     *
+     * @param {String|Function} value
+     * @param {Function} cb
+     */
+
+    Vue.prototype._resolveComponent = function (value, cb) {
+      var factory;
+      if (typeof value === 'function') {
+        factory = value;
+      } else {
+        factory = resolveAsset(this.$options, 'components', value, true);
+      }
+      /* istanbul ignore if */
+      if (!factory) {
+        return;
+      }
+      // async component factory
+      if (!factory.options) {
+        if (factory.resolved) {
+          // cached
+          cb(factory.resolved);
+        } else if (factory.requested) {
+          // pool callbacks
+          factory.pendingCallbacks.push(cb);
+        } else {
+          factory.requested = true;
+          var cbs = factory.pendingCallbacks = [cb];
+          factory.call(this, function resolve(res) {
+            if (isPlainObject(res)) {
+              res = Vue.extend(res);
+            }
+            // cache resolved
+            factory.resolved = res;
+            // invoke callbacks
+            for (var i = 0, l = cbs.length; i < l; i++) {
+              cbs[i](res);
+            }
+          }, function reject(reason) {
+            'development' !== 'production' && warn('Failed to resolve async component' + (typeof value === 'string' ? ': ' + value : '') + '. ' + (reason ? '\nReason: ' + reason : ''));
+          });
+        }
+      } else {
+        // normal component
+        cb(factory);
+      }
+    };
+  }
+
+  var filterRE$1 = /[^|]\|[^|]/;
+
+  function dataAPI (Vue) {
+    /**
+     * Get the value from an expression on this vm.
+     *
+     * @param {String} exp
+     * @param {Boolean} [asStatement]
+     * @return {*}
+     */
+
+    Vue.prototype.$get = function (exp, asStatement) {
+      var res = parseExpression(exp);
+      if (res) {
+        if (asStatement) {
+          var self = this;
+          return function statementHandler() {
+            self.$arguments = toArray(arguments);
+            var result = res.get.call(self, self);
+            self.$arguments = null;
+            return result;
+          };
+        } else {
+          try {
+            return res.get.call(this, this);
+          } catch (e) {}
+        }
+      }
+    };
+
+    /**
+     * Set the value from an expression on this vm.
+     * The expression must be a valid left-hand
+     * expression in an assignment.
+     *
+     * @param {String} exp
+     * @param {*} val
+     */
+
+    Vue.prototype.$set = function (exp, val) {
+      var res = parseExpression(exp, true);
+      if (res && res.set) {
+        res.set.call(this, this, val);
+      }
+    };
+
+    /**
+     * Delete a property on the VM
+     *
+     * @param {String} key
+     */
+
+    Vue.prototype.$delete = function (key) {
+      del(this._data, key);
+    };
+
+    /**
+     * Watch an expression, trigger callback when its
+     * value changes.
+     *
+     * @param {String|Function} expOrFn
+     * @param {Function} cb
+     * @param {Object} [options]
+     *                 - {Boolean} deep
+     *                 - {Boolean} immediate
+     * @return {Function} - unwatchFn
+     */
+
+    Vue.prototype.$watch = function (expOrFn, cb, options) {
+      var vm = this;
+      var parsed;
+      if (typeof expOrFn === 'string') {
+        parsed = parseDirective(expOrFn);
+        expOrFn = parsed.expression;
+      }
+      var watcher = new Watcher(vm, expOrFn, cb, {
+        deep: options && options.deep,
+        sync: options && options.sync,
+        filters: parsed && parsed.filters,
+        user: !options || options.user !== false
+      });
+      if (options && options.immediate) {
+        cb.call(vm, watcher.value);
+      }
+      return function unwatchFn() {
+        watcher.teardown();
+      };
+    };
+
+    /**
+     * Evaluate a text directive, including filters.
+     *
+     * @param {String} text
+     * @param {Boolean} [asStatement]
+     * @return {String}
+     */
+
+    Vue.prototype.$eval = function (text, asStatement) {
+      // check for filters.
+      if (filterRE$1.test(text)) {
+        var dir = parseDirective(text);
+        // the filter regex check might give false positive
+        // for pipes inside strings, so it's possible that
+        // we don't get any filters here
+        var val = this.$get(dir.expression, asStatement);
+        return dir.filters ? this._applyFilters(val, null, dir.filters) : val;
+      } else {
+        // no filter
+        return this.$get(text, asStatement);
+      }
+    };
+
+    /**
+     * Interpolate a piece of template text.
+     *
+     * @param {String} text
+     * @return {String}
+     */
+
+    Vue.prototype.$interpolate = function (text) {
+      var tokens = parseText(text);
+      var vm = this;
+      if (tokens) {
+        if (tokens.length === 1) {
+          return vm.$eval(tokens[0].value) + '';
+        } else {
+          return tokens.map(function (token) {
+            return token.tag ? vm.$eval(token.value) : token.value;
+          }).join('');
+        }
+      } else {
+        return text;
+      }
+    };
+
+    /**
+     * Log instance data as a plain JS object
+     * so that it is easier to inspect in console.
+     * This method assumes console is available.
+     *
+     * @param {String} [path]
+     */
+
+    Vue.prototype.$log = function (path) {
+      var data = path ? getPath(this._data, path) : this._data;
+      if (data) {
+        data = clean(data);
+      }
+      // include computed fields
+      if (!path) {
+        var key;
+        for (key in this.$options.computed) {
+          data[key] = clean(this[key]);
+        }
+        if (this._props) {
+          for (key in this._props) {
+            data[key] = clean(this[key]);
+          }
+        }
+      }
+      console.log(data);
+    };
+
+    /**
+     * "clean" a getter/setter converted object into a plain
+     * object copy.
+     *
+     * @param {Object} - obj
+     * @return {Object}
+     */
+
+    function clean(obj) {
+      return JSON.parse(JSON.stringify(obj));
+    }
+  }
+
+  function domAPI (Vue) {
+    /**
+     * Convenience on-instance nextTick. The callback is
+     * auto-bound to the instance, and this avoids component
+     * modules having to rely on the global Vue.
+     *
+     * @param {Function} fn
+     */
+
+    Vue.prototype.$nextTick = function (fn) {
+      nextTick(fn, this);
+    };
+
+    /**
+     * Append instance to target
+     *
+     * @param {Node} target
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition] - defaults to true
+     */
+
+    Vue.prototype.$appendTo = function (target, cb, withTransition) {
+      return insert(this, target, cb, withTransition, append, appendWithTransition);
+    };
+
+    /**
+     * Prepend instance to target
+     *
+     * @param {Node} target
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition] - defaults to true
+     */
+
+    Vue.prototype.$prependTo = function (target, cb, withTransition) {
+      target = query(target);
+      if (target.hasChildNodes()) {
+        this.$before(target.firstChild, cb, withTransition);
+      } else {
+        this.$appendTo(target, cb, withTransition);
+      }
+      return this;
+    };
+
+    /**
+     * Insert instance before target
+     *
+     * @param {Node} target
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition] - defaults to true
+     */
+
+    Vue.prototype.$before = function (target, cb, withTransition) {
+      return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);
+    };
+
+    /**
+     * Insert instance after target
+     *
+     * @param {Node} target
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition] - defaults to true
+     */
+
+    Vue.prototype.$after = function (target, cb, withTransition) {
+      target = query(target);
+      if (target.nextSibling) {
+        this.$before(target.nextSibling, cb, withTransition);
+      } else {
+        this.$appendTo(target.parentNode, cb, withTransition);
+      }
+      return this;
+    };
+
+    /**
+     * Remove instance from DOM
+     *
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition] - defaults to true
+     */
+
+    Vue.prototype.$remove = function (cb, withTransition) {
+      if (!this.$el.parentNode) {
+        return cb && cb();
+      }
+      var inDocument = this._isAttached && inDoc(this.$el);
+      // if we are not in document, no need to check
+      // for transitions
+      if (!inDocument) withTransition = false;
+      var self = this;
+      var realCb = function realCb() {
+        if (inDocument) self._callHook('detached');
+        if (cb) cb();
+      };
+      if (this._isFragment) {
+        removeNodeRange(this._fragmentStart, this._fragmentEnd, this, this._fragment, realCb);
+      } else {
+        var op = withTransition === false ? removeWithCb : removeWithTransition;
+        op(this.$el, this, realCb);
+      }
+      return this;
+    };
+
+    /**
+     * Shared DOM insertion function.
+     *
+     * @param {Vue} vm
+     * @param {Element} target
+     * @param {Function} [cb]
+     * @param {Boolean} [withTransition]
+     * @param {Function} op1 - op for non-transition insert
+     * @param {Function} op2 - op for transition insert
+     * @return vm
+     */
+
+    function insert(vm, target, cb, withTransition, op1, op2) {
+      target = query(target);
+      var targetIsDetached = !inDoc(target);
+      var op = withTransition === false || targetIsDetached ? op1 : op2;
+      var shouldCallHook = !targetIsDetached && !vm._isAttached && !inDoc(vm.$el);
+      if (vm._isFragment) {
+        mapNodeRange(vm._fragmentStart, vm._fragmentEnd, function (node) {
+          op(node, target, vm);
+        });
+        cb && cb();
+      } else {
+        op(vm.$el, target, vm, cb);
+      }
+      if (shouldCallHook) {
+        vm._callHook('attached');
+      }
+      return vm;
+    }
+
+    /**
+     * Check for selectors
+     *
+     * @param {String|Element} el
+     */
+
+    function query(el) {
+      return typeof el === 'string' ? document.querySelector(el) : el;
+    }
+
+    /**
+     * Append operation that takes a callback.
+     *
+     * @param {Node} el
+     * @param {Node} target
+     * @param {Vue} vm - unused
+     * @param {Function} [cb]
+     */
+
+    function append(el, target, vm, cb) {
+      target.appendChild(el);
+      if (cb) cb();
+    }
+
+    /**
+     * InsertBefore operation that takes a callback.
+     *
+     * @param {Node} el
+     * @param {Node} target
+     * @param {Vue} vm - unused
+     * @param {Function} [cb]
+     */
+
+    function beforeWithCb(el, target, vm, cb) {
+      before(el, target);
+      if (cb) cb();
+    }
+
+    /**
+     * Remove operation that takes a callback.
+     *
+     * @param {Node} el
+     * @param {Vue} vm - unused
+     * @param {Function} [cb]
+     */
+
+    function removeWithCb(el, vm, cb) {
+      remove(el);
+      if (cb) cb();
+    }
+  }
+
+  function eventsAPI (Vue) {
+    /**
+     * Listen on the given `event` with `fn`.
+     *
+     * @param {String} event
+     * @param {Function} fn
+     */
+
+    Vue.prototype.$on = function (event, fn) {
+      (this._events[event] || (this._events[event] = [])).push(fn);
+      modifyListenerCount(this, event, 1);
+      return this;
+    };
+
+    /**
+     * Adds an `event` listener that will be invoked a single
+     * time then automatically removed.
+     *
+     * @param {String} event
+     * @param {Function} fn
+     */
+
+    Vue.prototype.$once = function (event, fn) {
+      var self = this;
+      function on() {
+        self.$off(event, on);
+        fn.apply(this, arguments);
+      }
+      on.fn = fn;
+      this.$on(event, on);
+      return this;
+    };
+
+    /**
+     * Remove the given callback for `event` or all
+     * registered callbacks.
+     *
+     * @param {String} event
+     * @param {Function} fn
+     */
+
+    Vue.prototype.$off = function (event, fn) {
+      var cbs;
+      // all
+      if (!arguments.length) {
+        if (this.$parent) {
+          for (event in this._events) {
+            cbs = this._events[event];
+            if (cbs) {
+              modifyListenerCount(this, event, -cbs.length);
+            }
+          }
+        }
+        this._events = {};
+        return this;
+      }
+      // specific event
+      cbs = this._events[event];
+      if (!cbs) {
+        return this;
+      }
+      if (arguments.length === 1) {
+        modifyListenerCount(this, event, -cbs.length);
+        this._events[event] = null;
+        return this;
+      }
+      // specific handler
+      var cb;
+      var i = cbs.length;
+      while (i--) {
+        cb = cbs[i];
+        if (cb === fn || cb.fn === fn) {
+          modifyListenerCount(this, event, -1);
+          cbs.splice(i, 1);
+          break;
+        }
+      }
+      return this;
+    };
+
+    /**
+     * Trigger an event on self.
+     *
+     * @param {String|Object} event
+     * @return {Boolean} shouldPropagate
+     */
+
+    Vue.prototype.$emit = function (event) {
+      var isSource = typeof event === 'string';
+      event = isSource ? event : event.name;
+      var cbs = this._events[event];
+      var shouldPropagate = isSource || !cbs;
+      if (cbs) {
+        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
+        // this is a somewhat hacky solution to the question raised
+        // in #2102: for an inline component listener like <comp @test="doThis">,
+        // the propagation handling is somewhat broken. Therefore we
+        // need to treat these inline callbacks differently.
+        var hasParentCbs = isSource && cbs.some(function (cb) {
+          return cb._fromParent;
+        });
+        if (hasParentCbs) {
+          shouldPropagate = false;
+        }
+        var args = toArray(arguments, 1);
+        for (var i = 0, l = cbs.length; i < l; i++) {
+          var cb = cbs[i];
+          var res = cb.apply(this, args);
+          if (res === true && (!hasParentCbs || cb._fromParent)) {
+            shouldPropagate = true;
+          }
+        }
+      }
+      return shouldPropagate;
+    };
+
+    /**
+     * Recursively broadcast an event to all children instances.
+     *
+     * @param {String|Object} event
+     * @param {...*} additional arguments
+     */
+
+    Vue.prototype.$broadcast = function (event) {
+      var isSource = typeof event === 'string';
+      event = isSource ? event : event.name;
+      // if no child has registered for this event,
+      // then there's no need to broadcast.
+      if (!this._eventsCount[event]) return;
+      var children = this.$children;
+      var args = toArray(arguments);
+      if (isSource) {
+        // use object event to indicate non-source emit
+        // on children
+        args[0] = { name: event, source: this };
+      }
+      for (var i = 0, l = children.length; i < l; i++) {
+        var child = children[i];
+        var shouldPropagate = child.$emit.apply(child, args);
+        if (shouldPropagate) {
+          child.$broadcast.apply(child, args);
+        }
+      }
+      return this;
+    };
+
+    /**
+     * Recursively propagate an event up the parent chain.
+     *
+     * @param {String} event
+     * @param {...*} additional arguments
+     */
+
+    Vue.prototype.$dispatch = function (event) {
+      var shouldPropagate = this.$emit.apply(this, arguments);
+      if (!shouldPropagate) return;
+      var parent = this.$parent;
+      var args = toArray(arguments);
+      // use object event to indicate non-source emit
+      // on parents
+      args[0] = { name: event, source: this };
+      while (parent) {
+        shouldPropagate = parent.$emit.apply(parent, args);
+        parent = shouldPropagate ? parent.$parent : null;
+      }
+      return this;
+    };
+
+    /**
+     * Modify the listener counts on all parents.
+     * This bookkeeping allows $broadcast to return early when
+     * no child has listened to a certain event.
+     *
+     * @param {Vue} vm
+     * @param {String} event
+     * @param {Number} count
+     */
+
+    var hookRE = /^hook:/;
+    function modifyListenerCount(vm, event, count) {
+      var parent = vm.$parent;
+      // hooks do not get broadcasted so no need
+      // to do bookkeeping for them
+      if (!parent || !count || hookRE.test(event)) return;
+      while (parent) {
+        parent._eventsCount[event] = (parent._eventsCount[event] || 0) + count;
+        parent = parent.$parent;
+      }
+    }
+  }
+
+  function lifecycleAPI (Vue) {
+    /**
+     * Set instance target element and kick off the compilation
+     * process. The passed in `el` can be a selector string, an
+     * existing Element, or a DocumentFragment (for block
+     * instances).
+     *
+     * @param {Element|DocumentFragment|string} el
+     * @public
+     */
+
+    Vue.prototype.$mount = function (el) {
+      if (this._isCompiled) {
+        'development' !== 'production' && warn('$mount() should be called only once.', this);
+        return;
+      }
+      el = query(el);
+      if (!el) {
+        el = document.createElement('div');
+      }
+      this._compile(el);
+      this._initDOMHooks();
+      if (inDoc(this.$el)) {
+        this._callHook('attached');
+        ready.call(this);
+      } else {
+        this.$once('hook:attached', ready);
+      }
+      return this;
+    };
+
+    /**
+     * Mark an instance as ready.
+     */
+
+    function ready() {
+      this._isAttached = true;
+      this._isReady = true;
+      this._callHook('ready');
+    }
+
+    /**
+     * Teardown the instance, simply delegate to the internal
+     * _destroy.
+     *
+     * @param {Boolean} remove
+     * @param {Boolean} deferCleanup
+     */
+
+    Vue.prototype.$destroy = function (remove, deferCleanup) {
+      this._destroy(remove, deferCleanup);
+    };
+
+    /**
+     * Partially compile a piece of DOM and return a
+     * decompile function.
+     *
+     * @param {Element|DocumentFragment} el
+     * @param {Vue} [host]
+     * @param {Object} [scope]
+     * @param {Fragment} [frag]
+     * @return {Function}
+     */
+
+    Vue.prototype.$compile = function (el, host, scope, frag) {
+      return compile(el, this.$options, true)(this, el, host, scope, frag);
+    };
+  }
+
+  /**
+   * The exposed Vue constructor.
+   *
+   * API conventions:
+   * - public API methods/properties are prefixed with `$`
+   * - internal methods/properties are prefixed with `_`
+   * - non-prefixed properties are assumed to be proxied user
+   *   data.
+   *
+   * @constructor
+   * @param {Object} [options]
+   * @public
+   */
+
+  function Vue(options) {
+    this._init(options);
+  }
+
+  // install internals
+  initMixin(Vue);
+  stateMixin(Vue);
+  eventsMixin(Vue);
+  lifecycleMixin(Vue);
+  miscMixin(Vue);
+
+  // install instance APIs
+  dataAPI(Vue);
+  domAPI(Vue);
+  eventsAPI(Vue);
+  lifecycleAPI(Vue);
+
+  var slot = {
+
+    priority: SLOT,
+    params: ['name'],
+
+    bind: function bind() {
+      // this was resolved during component transclusion
+      var name = this.params.name || 'default';
+      var content = this.vm._slotContents && this.vm._slotContents[name];
+      if (!content || !content.hasChildNodes()) {
+        this.fallback();
+      } else {
+        this.compile(content.cloneNode(true), this.vm._context, this.vm);
+      }
+    },
+
+    compile: function compile(content, context, host) {
+      if (content && context) {
+        if (this.el.hasChildNodes() && content.childNodes.length === 1 && content.childNodes[0].nodeType === 1 && content.childNodes[0].hasAttribute('v-if')) {
+          // if the inserted slot has v-if
+          // inject fallback content as the v-else
+          var elseBlock = document.createElement('template');
+          elseBlock.setAttribute('v-else', '');
+          elseBlock.innerHTML = this.el.innerHTML;
+          // the else block should be compiled in child scope
+          elseBlock._context = this.vm;
+          content.appendChild(elseBlock);
+        }
+        var scope = host ? host._scope : this._scope;
+        this.unlink = context.$compile(content, host, scope, this._frag);
+      }
+      if (content) {
+        replace(this.el, content);
+      } else {
+        remove(this.el);
+      }
+    },
+
+    fallback: function fallback() {
+      this.compile(extractContent(this.el, true), this.vm);
+    },
+
+    unbind: function unbind() {
+      if (this.unlink) {
+        this.unlink();
+      }
+    }
+  };
+
+  var partial = {
+
+    priority: PARTIAL,
+
+    params: ['name'],
+
+    // watch changes to name for dynamic partials
+    paramWatchers: {
+      name: function name(value) {
+        vIf.remove.call(this);
+        if (value) {
+          this.insert(value);
+        }
+      }
+    },
+
+    bind: function bind() {
+      this.anchor = createAnchor('v-partial');
+      replace(this.el, this.anchor);
+      this.insert(this.params.name);
+    },
+
+    insert: function insert(id) {
+      var partial = resolveAsset(this.vm.$options, 'partials', id, true);
+      if (partial) {
+        this.factory = new FragmentFactory(this.vm, partial);
+        vIf.insert.call(this);
+      }
+    },
+
+    unbind: function unbind() {
+      if (this.frag) {
+        this.frag.destroy();
+      }
+    }
+  };
+
+  var elementDirectives = {
+    slot: slot,
+    partial: partial
+  };
+
+  var convertArray = vFor._postProcess;
+
+  /**
+   * Limit filter for arrays
+   *
+   * @param {Number} n
+   * @param {Number} offset (Decimal expected)
+   */
+
+  function limitBy(arr, n, offset) {
+    offset = offset ? parseInt(offset, 10) : 0;
+    n = toNumber(n);
+    return typeof n === 'number' ? arr.slice(offset, offset + n) : arr;
+  }
+
+  /**
+   * Filter filter for arrays
+   *
+   * @param {String} search
+   * @param {String} [delimiter]
+   * @param {String} ...dataKeys
+   */
+
+  function filterBy(arr, search, delimiter) {
+    arr = convertArray(arr);
+    if (search == null) {
+      return arr;
+    }
+    if (typeof search === 'function') {
+      return arr.filter(search);
+    }
+    // cast to lowercase string
+    search = ('' + search).toLowerCase();
+    // allow optional `in` delimiter
+    // because why not
+    var n = delimiter === 'in' ? 3 : 2;
+    // extract and flatten keys
+    var keys = Array.prototype.concat.apply([], toArray(arguments, n));
+    var res = [];
+    var item, key, val, j;
+    for (var i = 0, l = arr.length; i < l; i++) {
+      item = arr[i];
+      val = item && item.$value || item;
+      j = keys.length;
+      if (j) {
+        while (j--) {
+          key = keys[j];
+          if (key === '$key' && contains(item.$key, search) || contains(getPath(val, key), search)) {
+            res.push(item);
+            break;
+          }
+        }
+      } else if (contains(item, search)) {
+        res.push(item);
+      }
+    }
+    return res;
+  }
+
+  /**
+   * Filter filter for arrays
+   *
+   * @param {String|Array<String>|Function} ...sortKeys
+   * @param {Number} [order]
+   */
+
+  function orderBy(arr) {
+    var comparator = null;
+    var sortKeys = undefined;
+    arr = convertArray(arr);
+
+    // determine order (last argument)
+    var args = toArray(arguments, 1);
+    var order = args[args.length - 1];
+    if (typeof order === 'number') {
+      order = order < 0 ? -1 : 1;
+      args = args.length > 1 ? args.slice(0, -1) : args;
+    } else {
+      order = 1;
+    }
+
+    // determine sortKeys & comparator
+    var firstArg = args[0];
+    if (!firstArg) {
+      return arr;
+    } else if (typeof firstArg === 'function') {
+      // custom comparator
+      comparator = function (a, b) {
+        return firstArg(a, b) * order;
+      };
+    } else {
+      // string keys. flatten first
+      sortKeys = Array.prototype.concat.apply([], args);
+      comparator = function (a, b, i) {
+        i = i || 0;
+        return i >= sortKeys.length - 1 ? baseCompare(a, b, i) : baseCompare(a, b, i) || comparator(a, b, i + 1);
+      };
+    }
+
+    function baseCompare(a, b, sortKeyIndex) {
+      var sortKey = sortKeys[sortKeyIndex];
+      if (sortKey) {
+        if (sortKey !== '$key') {
+          if (isObject(a) && '$value' in a) a = a.$value;
+          if (isObject(b) && '$value' in b) b = b.$value;
+        }
+        a = isObject(a) ? getPath(a, sortKey) : a;
+        b = isObject(b) ? getPath(b, sortKey) : b;
+      }
+      return a === b ? 0 : a > b ? order : -order;
+    }
+
+    // sort on a copy to avoid mutating original array
+    return arr.slice().sort(comparator);
+  }
+
+  /**
+   * String contain helper
+   *
+   * @param {*} val
+   * @param {String} search
+   */
+
+  function contains(val, search) {
+    var i;
+    if (isPlainObject(val)) {
+      var keys = Object.keys(val);
+      i = keys.length;
+      while (i--) {
+        if (contains(val[keys[i]], search)) {
+          return true;
+        }
+      }
+    } else if (isArray(val)) {
+      i = val.length;
+      while (i--) {
+        if (contains(val[i], search)) {
+          return true;
+        }
+      }
+    } else if (val != null) {
+      return val.toString().toLowerCase().indexOf(search) > -1;
+    }
+  }
+
+  var digitsRE = /(\d{3})(?=\d)/g;
+
+  // asset collections must be a plain object.
+  var filters = {
+
+    orderBy: orderBy,
+    filterBy: filterBy,
+    limitBy: limitBy,
+
+    /**
+     * Stringify value.
+     *
+     * @param {Number} indent
+     */
+
+    json: {
+      read: function read(value, indent) {
+        return typeof value === 'string' ? value : JSON.stringify(value, null, arguments.length > 1 ? indent : 2);
+      },
+      write: function write(value) {
+        try {
+          return JSON.parse(value);
+        } catch (e) {
+          return value;
+        }
+      }
+    },
+
+    /**
+     * 'abc' => 'Abc'
+     */
+
+    capitalize: function capitalize(value) {
+      if (!value && value !== 0) return '';
+      value = value.toString();
+      return value.charAt(0).toUpperCase() + value.slice(1);
+    },
+
+    /**
+     * 'abc' => 'ABC'
+     */
+
+    uppercase: function uppercase(value) {
+      return value || value === 0 ? value.toString().toUpperCase() : '';
+    },
+
+    /**
+     * 'AbC' => 'abc'
+     */
+
+    lowercase: function lowercase(value) {
+      return value || value === 0 ? value.toString().toLowerCase() : '';
+    },
+
+    /**
+     * 12345 => $12,345.00
+     *
+     * @param {String} sign
+     * @param {Number} decimals Decimal places
+     */
+
+    currency: function currency(value, _currency, decimals) {
+      value = parseFloat(value);
+      if (!isFinite(value) || !value && value !== 0) return '';
+      _currency = _currency != null ? _currency : '$';
+      decimals = decimals != null ? decimals : 2;
+      var stringified = Math.abs(value).toFixed(decimals);
+      var _int = decimals ? stringified.slice(0, -1 - decimals) : stringified;
+      var i = _int.length % 3;
+      var head = i > 0 ? _int.slice(0, i) + (_int.length > 3 ? ',' : '') : '';
+      var _float = decimals ? stringified.slice(-1 - decimals) : '';
+      var sign = value < 0 ? '-' : '';
+      return sign + _currency + head + _int.slice(i).replace(digitsRE, '$1,') + _float;
+    },
+
+    /**
+     * 'item' => 'items'
+     *
+     * @params
+     *  an array of strings corresponding to
+     *  the single, double, triple ... forms of the word to
+     *  be pluralized. When the number to be pluralized
+     *  exceeds the length of the args, it will use the last
+     *  entry in the array.
+     *
+     *  e.g. ['single', 'double', 'triple', 'multiple']
+     */
+
+    pluralize: function pluralize(value) {
+      var args = toArray(arguments, 1);
+      var length = args.length;
+      if (length > 1) {
+        var index = value % 10 - 1;
+        return index in args ? args[index] : args[length - 1];
+      } else {
+        return args[0] + (value === 1 ? '' : 's');
+      }
+    },
+
+    /**
+     * Debounce a handler function.
+     *
+     * @param {Function} handler
+     * @param {Number} delay = 300
+     * @return {Function}
+     */
+
+    debounce: function debounce(handler, delay) {
+      if (!handler) return;
+      if (!delay) {
+        delay = 300;
+      }
+      return _debounce(handler, delay);
+    }
+  };
+
+  function installGlobalAPI (Vue) {
+    /**
+     * Vue and every constructor that extends Vue has an
+     * associated options object, which can be accessed during
+     * compilation steps as `this.constructor.options`.
+     *
+     * These can be seen as the default options of every
+     * Vue instance.
+     */
+
+    Vue.options = {
+      directives: directives,
+      elementDirectives: elementDirectives,
+      filters: filters,
+      transitions: {},
+      components: {},
+      partials: {},
+      replace: true
+    };
+
+    /**
+     * Expose useful internals
+     */
+
+    Vue.util = util;
+    Vue.config = config;
+    Vue.set = set;
+    Vue['delete'] = del;
+    Vue.nextTick = nextTick;
+
+    /**
+     * The following are exposed for advanced usage / plugins
+     */
+
+    Vue.compiler = compiler;
+    Vue.FragmentFactory = FragmentFactory;
+    Vue.internalDirectives = internalDirectives;
+    Vue.parsers = {
+      path: path,
+      text: text,
+      template: template,
+      directive: directive,
+      expression: expression
+    };
+
+    /**
+     * Each instance constructor, including Vue, has a unique
+     * cid. This enables us to create wrapped "child
+     * constructors" for prototypal inheritance and cache them.
+     */
+
+    Vue.cid = 0;
+    var cid = 1;
+
+    /**
+     * Class inheritance
+     *
+     * @param {Object} extendOptions
+     */
+
+    Vue.extend = function (extendOptions) {
+      extendOptions = extendOptions || {};
+      var Super = this;
+      var isFirstExtend = Super.cid === 0;
+      if (isFirstExtend && extendOptions._Ctor) {
+        return extendOptions._Ctor;
+      }
+      var name = extendOptions.name || Super.options.name;
+      if ('development' !== 'production') {
+        if (!/^[a-zA-Z][\w-]*$/.test(name)) {
+          warn('Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characaters and the hyphen.');
+          name = null;
+        }
+      }
+      var Sub = createClass(name || 'VueComponent');
+      Sub.prototype = Object.create(Super.prototype);
+      Sub.prototype.constructor = Sub;
+      Sub.cid = cid++;
+      Sub.options = mergeOptions(Super.options, extendOptions);
+      Sub['super'] = Super;
+      // allow further extension
+      Sub.extend = Super.extend;
+      // create asset registers, so extended classes
+      // can have their private assets too.
+      config._assetTypes.forEach(function (type) {
+        Sub[type] = Super[type];
+      });
+      // enable recursive self-lookup
+      if (name) {
+        Sub.options.components[name] = Sub;
+      }
+      // cache constructor
+      if (isFirstExtend) {
+        extendOptions._Ctor = Sub;
+      }
+      return Sub;
+    };
+
+    /**
+     * A function that returns a sub-class constructor with the
+     * given name. This gives us much nicer output when
+     * logging instances in the console.
+     *
+     * @param {String} name
+     * @return {Function}
+     */
+
+    function createClass(name) {
+      /* eslint-disable no-new-func */
+      return new Function('return function ' + classify(name) + ' (options) { this._init(options) }')();
+      /* eslint-enable no-new-func */
+    }
+
+    /**
+     * Plugin system
+     *
+     * @param {Object} plugin
+     */
+
+    Vue.use = function (plugin) {
+      /* istanbul ignore if */
+      if (plugin.installed) {
+        return;
+      }
+      // additional parameters
+      var args = toArray(arguments, 1);
+      args.unshift(this);
+      if (typeof plugin.install === 'function') {
+        plugin.install.apply(plugin, args);
+      } else {
+        plugin.apply(null, args);
+      }
+      plugin.installed = true;
+      return this;
+    };
+
+    /**
+     * Apply a global mixin by merging it into the default
+     * options.
+     */
+
+    Vue.mixin = function (mixin) {
+      Vue.options = mergeOptions(Vue.options, mixin);
+    };
+
+    /**
+     * Create asset registration methods with the following
+     * signature:
+     *
+     * @param {String} id
+     * @param {*} definition
+     */
+
+    config._assetTypes.forEach(function (type) {
+      Vue[type] = function (id, definition) {
+        if (!definition) {
+          return this.options[type + 's'][id];
+        } else {
+          /* istanbul ignore if */
+          if ('development' !== 'production') {
+            if (type === 'component' && (commonTagRE.test(id) || reservedTagRE.test(id))) {
+              warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + id);
+            }
+          }
+          if (type === 'component' && isPlainObject(definition)) {
+            if (!definition.name) {
+              definition.name = id;
+            }
+            definition = Vue.extend(definition);
+          }
+          this.options[type + 's'][id] = definition;
+          return definition;
+        }
+      };
+    });
+
+    // expose internal transition API
+    extend(Vue.transition, transition);
+  }
+
+  installGlobalAPI(Vue);
+
+  Vue.version = '1.0.26';
+
+  // devtools global hook
+  /* istanbul ignore next */
+  setTimeout(function () {
+    if (config.devtools) {
+      if (devtools) {
+        devtools.emit('init', Vue);
+      } else if ('development' !== 'production' && inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)) {
+        console.log('Download the Vue Devtools for a better development experience:\n' + 'https://github.com/vuejs/vue-devtools');
+      }
+    }
+  }, 0);
+
+  return Vue;
+
+}));
\ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.js.erb b/vendor/assets/javascripts/vue.js.erb
new file mode 100644
index 0000000000000000000000000000000000000000..008beb10f4d1a7213df6a010a507f3e4559936af
--- /dev/null
+++ b/vendor/assets/javascripts/vue.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..2c9a8a0e117b03852a86c11ad83945d5f8ed83d9
--- /dev/null
+++ b/vendor/assets/javascripts/vue.min.js
@@ -0,0 +1,9 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 2016 Evan You
+ * Released under the MIT License.
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vue=e()}(this,function(){"use strict";function t(e,n,r){if(i(e,n))return void(e[n]=r);if(e._isVue)return void t(e._data,n,r);var s=e.__ob__;if(!s)return void(e[n]=r);if(s.convert(n,r),s.dep.notify(),s.vms)for(var o=s.vms.length;o--;){var a=s.vms[o];a._proxy(n),a._digest()}return r}function e(t,e){if(i(t,e)){delete t[e];var n=t.__ob__;if(!n)return void(t._isVue&&(delete t._data[e],t._digest()));if(n.dep.notify(),n.vms)for(var r=n.vms.length;r--;){var s=n.vms[r];s._unproxy(e),s._digest()}}}function i(t,e){return Oi.call(t,e)}function n(t){return Ti.test(t)}function r(t){var e=(t+"").charCodeAt(0);return 36===e||95===e}function s(t){return null==t?"":t.toString()}function o(t){if("string"!=typeof t)return t;var e=Number(t);return isNaN(e)?t:e}function a(t){return"true"===t?!0:"false"===t?!1:t}function h(t){var e=t.charCodeAt(0),i=t.charCodeAt(t.length-1);return e!==i||34!==e&&39!==e?t:t.slice(1,-1)}function l(t){return t.replace(Ni,c)}function c(t,e){return e?e.toUpperCase():""}function u(t){return t.replace(ji,"$1-$2").toLowerCase()}function f(t){return t.replace(Ei,c)}function p(t,e){return function(i){var n=arguments.length;return n?n>1?t.apply(e,arguments):t.call(e,i):t.call(e)}}function d(t,e){e=e||0;for(var i=t.length-e,n=new Array(i);i--;)n[i]=t[i+e];return n}function v(t,e){for(var i=Object.keys(e),n=i.length;n--;)t[i[n]]=e[i[n]];return t}function m(t){return null!==t&&"object"==typeof t}function g(t){return Si.call(t)===Fi}function _(t,e,i,n){Object.defineProperty(t,e,{value:i,enumerable:!!n,writable:!0,configurable:!0})}function y(t,e){var i,n,r,s,o,a=function h(){var a=Date.now()-s;e>a&&a>=0?i=setTimeout(h,e-a):(i=null,o=t.apply(r,n),i||(r=n=null))};return function(){return r=this,n=arguments,s=Date.now(),i||(i=setTimeout(a,e)),o}}function b(t,e){for(var i=t.length;i--;)if(t[i]===e)return i;return-1}function w(t){var e=function i(){return i.cancelled?void 0:t.apply(this,arguments)};return e.cancel=function(){e.cancelled=!0},e}function C(t,e){return t==e||(m(t)&&m(e)?JSON.stringify(t)===JSON.stringify(e):!1)}function $(t){this.size=0,this.limit=t,this.head=this.tail=void 0,this._keymap=Object.create(null)}function k(){var t,e=en.slice(hn,on).trim();if(e){t={};var i=e.match(vn);t.name=i[0],i.length>1&&(t.args=i.slice(1).map(x))}t&&(nn.filters=nn.filters||[]).push(t),hn=on+1}function x(t){if(mn.test(t))return{value:o(t),dynamic:!1};var e=h(t),i=e===t;return{value:i?t:e,dynamic:i}}function A(t){var e=dn.get(t);if(e)return e;for(en=t,ln=cn=!1,un=fn=pn=0,hn=0,nn={},on=0,an=en.length;an>on;on++)if(sn=rn,rn=en.charCodeAt(on),ln)39===rn&&92!==sn&&(ln=!ln);else if(cn)34===rn&&92!==sn&&(cn=!cn);else if(124===rn&&124!==en.charCodeAt(on+1)&&124!==en.charCodeAt(on-1))null==nn.expression?(hn=on+1,nn.expression=en.slice(0,on).trim()):k();else switch(rn){case 34:cn=!0;break;case 39:ln=!0;break;case 40:pn++;break;case 41:pn--;break;case 91:fn++;break;case 93:fn--;break;case 123:un++;break;case 125:un--}return null==nn.expression?nn.expression=en.slice(0,on).trim():0!==hn&&k(),dn.put(t,nn),nn}function O(t){return t.replace(_n,"\\$&")}function T(){var t=O(An.delimiters[0]),e=O(An.delimiters[1]),i=O(An.unsafeDelimiters[0]),n=O(An.unsafeDelimiters[1]);bn=new RegExp(i+"((?:.|\\n)+?)"+n+"|"+t+"((?:.|\\n)+?)"+e,"g"),wn=new RegExp("^"+i+"((?:.|\\n)+?)"+n+"$"),yn=new $(1e3)}function N(t){yn||T();var e=yn.get(t);if(e)return e;if(!bn.test(t))return null;for(var i,n,r,s,o,a,h=[],l=bn.lastIndex=0;i=bn.exec(t);)n=i.index,n>l&&h.push({value:t.slice(l,n)}),r=wn.test(i[0]),s=r?i[1]:i[2],o=s.charCodeAt(0),a=42===o,s=a?s.slice(1):s,h.push({tag:!0,value:s.trim(),html:r,oneTime:a}),l=n+i[0].length;return l<t.length&&h.push({value:t.slice(l)}),yn.put(t,h),h}function j(t,e){return t.length>1?t.map(function(t){return E(t,e)}).join("+"):E(t[0],e,!0)}function E(t,e,i){return t.tag?t.oneTime&&e?'"'+e.$eval(t.value)+'"':S(t.value,i):'"'+t.value+'"'}function S(t,e){if(Cn.test(t)){var i=A(t);return i.filters?"this._applyFilters("+i.expression+",null,"+JSON.stringify(i.filters)+",false)":"("+t+")"}return e?t:"("+t+")"}function F(t,e,i,n){R(t,1,function(){e.appendChild(t)},i,n)}function D(t,e,i,n){R(t,1,function(){B(t,e)},i,n)}function P(t,e,i){R(t,-1,function(){z(t)},e,i)}function R(t,e,i,n,r){var s=t.__v_trans;if(!s||!s.hooks&&!qi||!n._isCompiled||n.$parent&&!n.$parent._isCompiled)return i(),void(r&&r());var o=e>0?"enter":"leave";s[o](i,r)}function L(t){if("string"==typeof t){t=document.querySelector(t)}return t}function H(t){if(!t)return!1;var e=t.ownerDocument.documentElement,i=t.parentNode;return e===t||e===i||!(!i||1!==i.nodeType||!e.contains(i))}function I(t,e){var i=t.getAttribute(e);return null!==i&&t.removeAttribute(e),i}function M(t,e){var i=I(t,":"+e);return null===i&&(i=I(t,"v-bind:"+e)),i}function V(t,e){return t.hasAttribute(e)||t.hasAttribute(":"+e)||t.hasAttribute("v-bind:"+e)}function B(t,e){e.parentNode.insertBefore(t,e)}function W(t,e){e.nextSibling?B(t,e.nextSibling):e.parentNode.appendChild(t)}function z(t){t.parentNode.removeChild(t)}function U(t,e){e.firstChild?B(t,e.firstChild):e.appendChild(t)}function J(t,e){var i=t.parentNode;i&&i.replaceChild(e,t)}function q(t,e,i,n){t.addEventListener(e,i,n)}function Q(t,e,i){t.removeEventListener(e,i)}function G(t){var e=t.className;return"object"==typeof e&&(e=e.baseVal||""),e}function Z(t,e){Mi&&!/svg$/.test(t.namespaceURI)?t.className=e:t.setAttribute("class",e)}function X(t,e){if(t.classList)t.classList.add(e);else{var i=" "+G(t)+" ";i.indexOf(" "+e+" ")<0&&Z(t,(i+e).trim())}}function Y(t,e){if(t.classList)t.classList.remove(e);else{for(var i=" "+G(t)+" ",n=" "+e+" ";i.indexOf(n)>=0;)i=i.replace(n," ");Z(t,i.trim())}t.className||t.removeAttribute("class")}function K(t,e){var i,n;if(it(t)&&at(t.content)&&(t=t.content),t.hasChildNodes())for(tt(t),n=e?document.createDocumentFragment():document.createElement("div");i=t.firstChild;)n.appendChild(i);return n}function tt(t){for(var e;e=t.firstChild,et(e);)t.removeChild(e);for(;e=t.lastChild,et(e);)t.removeChild(e)}function et(t){return t&&(3===t.nodeType&&!t.data.trim()||8===t.nodeType)}function it(t){return t.tagName&&"template"===t.tagName.toLowerCase()}function nt(t,e){var i=An.debug?document.createComment(t):document.createTextNode(e?" ":"");return i.__v_anchor=!0,i}function rt(t){if(t.hasAttributes())for(var e=t.attributes,i=0,n=e.length;n>i;i++){var r=e[i].name;if(Nn.test(r))return l(r.replace(Nn,""))}}function st(t,e,i){for(var n;t!==e;)n=t.nextSibling,i(t),t=n;i(e)}function ot(t,e,i,n,r){function s(){if(a++,o&&a>=h.length){for(var t=0;t<h.length;t++)n.appendChild(h[t]);r&&r()}}var o=!1,a=0,h=[];st(t,e,function(t){t===e&&(o=!0),h.push(t),P(t,i,s)})}function at(t){return t&&11===t.nodeType}function ht(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}function lt(t,e){var i=t.tagName.toLowerCase(),n=t.hasAttributes();if(jn.test(i)||En.test(i)){if(n)return ct(t,e)}else{if(gt(e,"components",i))return{id:i};var r=n&&ct(t,e);if(r)return r}}function ct(t,e){var i=t.getAttribute("is");if(null!=i){if(gt(e,"components",i))return t.removeAttribute("is"),{id:i}}else if(i=M(t,"is"),null!=i)return{id:i,dynamic:!0}}function ut(e,n){var r,s,o;for(r in n)s=e[r],o=n[r],i(e,r)?m(s)&&m(o)&&ut(s,o):t(e,r,o);return e}function ft(t,e){var i=Object.create(t||null);return e?v(i,vt(e)):i}function pt(t){if(t.components)for(var e,i=t.components=vt(t.components),n=Object.keys(i),r=0,s=n.length;s>r;r++){var o=n[r];jn.test(o)||En.test(o)||(e=i[o],g(e)&&(i[o]=wi.extend(e)))}}function dt(t){var e,i,n=t.props;if(Di(n))for(t.props={},e=n.length;e--;)i=n[e],"string"==typeof i?t.props[i]=null:i.name&&(t.props[i.name]=i);else if(g(n)){var r=Object.keys(n);for(e=r.length;e--;)i=n[r[e]],"function"==typeof i&&(n[r[e]]={type:i})}}function vt(t){if(Di(t)){for(var e,i={},n=t.length;n--;){e=t[n];var r="function"==typeof e?e.options&&e.options.name||e.id:e.name||e.id;r&&(i[r]=e)}return i}return t}function mt(t,e,n){function r(i){var r=Sn[i]||Fn;o[i]=r(t[i],e[i],n,i)}pt(e),dt(e);var s,o={};if(e["extends"]&&(t="function"==typeof e["extends"]?mt(t,e["extends"].options,n):mt(t,e["extends"],n)),e.mixins)for(var a=0,h=e.mixins.length;h>a;a++){var l=e.mixins[a],c=l.prototype instanceof wi?l.options:l;t=mt(t,c,n)}for(s in t)r(s);for(s in e)i(t,s)||r(s);return o}function gt(t,e,i,n){if("string"==typeof i){var r,s=t[e],o=s[i]||s[r=l(i)]||s[r.charAt(0).toUpperCase()+r.slice(1)];return o}}function _t(){this.id=Dn++,this.subs=[]}function yt(t){Hn=!1,t(),Hn=!0}function bt(t){if(this.value=t,this.dep=new _t,_(t,"__ob__",this),Di(t)){var e=Pi?wt:Ct;e(t,Rn,Ln),this.observeArray(t)}else this.walk(t)}function wt(t,e){t.__proto__=e}function Ct(t,e,i){for(var n=0,r=i.length;r>n;n++){var s=i[n];_(t,s,e[s])}}function $t(t,e){if(t&&"object"==typeof t){var n;return i(t,"__ob__")&&t.__ob__ instanceof bt?n=t.__ob__:Hn&&(Di(t)||g(t))&&Object.isExtensible(t)&&!t._isVue&&(n=new bt(t)),n&&e&&n.addVm(e),n}}function kt(t,e,i){var n=new _t,r=Object.getOwnPropertyDescriptor(t,e);if(!r||r.configurable!==!1){var s=r&&r.get,o=r&&r.set,a=$t(i);Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){var e=s?s.call(t):i;if(_t.target&&(n.depend(),a&&a.dep.depend(),Di(e)))for(var r,o=0,h=e.length;h>o;o++)r=e[o],r&&r.__ob__&&r.__ob__.dep.depend();return e},set:function(e){var r=s?s.call(t):i;e!==r&&(o?o.call(t,e):i=e,a=$t(e),n.notify())}})}}function xt(t){t.prototype._init=function(t){t=t||{},this.$el=null,this.$parent=t.parent,this.$root=this.$parent?this.$parent.$root:this,this.$children=[],this.$refs={},this.$els={},this._watchers=[],this._directives=[],this._uid=Mn++,this._isVue=!0,this._events={},this._eventsCount={},this._isFragment=!1,this._fragment=this._fragmentStart=this._fragmentEnd=null,this._isCompiled=this._isDestroyed=this._isReady=this._isAttached=this._isBeingDestroyed=this._vForRemoving=!1,this._unlinkFn=null,this._context=t._context||this.$parent,this._scope=t._scope,this._frag=t._frag,this._frag&&this._frag.children.push(this),this.$parent&&this.$parent.$children.push(this),t=this.$options=mt(this.constructor.options,t,this),this._updateRef(),this._data={},this._callHook("init"),this._initState(),this._initEvents(),this._callHook("created"),t.el&&this.$mount(t.el)}}function At(t){if(void 0===t)return"eof";var e=t.charCodeAt(0);switch(e){case 91:case 93:case 46:case 34:case 39:case 48:return t;case 95:case 36:return"ident";case 32:case 9:case 10:case 13:case 160:case 65279:case 8232:case 8233:return"ws"}return e>=97&&122>=e||e>=65&&90>=e?"ident":e>=49&&57>=e?"number":"else"}function Ot(t){var e=t.trim();return"0"===t.charAt(0)&&isNaN(t)?!1:n(e)?h(e):"*"+e}function Tt(t){function e(){var e=t[c+1];return u===Xn&&"'"===e||u===Yn&&'"'===e?(c++,n="\\"+e,p[Bn](),!0):void 0}var i,n,r,s,o,a,h,l=[],c=-1,u=Jn,f=0,p=[];for(p[Wn]=function(){void 0!==r&&(l.push(r),r=void 0)},p[Bn]=function(){void 0===r?r=n:r+=n},p[zn]=function(){p[Bn](),f++},p[Un]=function(){if(f>0)f--,u=Zn,p[Bn]();else{if(f=0,r=Ot(r),r===!1)return!1;p[Wn]()}};null!=u;)if(c++,i=t[c],"\\"!==i||!e()){if(s=At(i),h=er[u],o=h[s]||h["else"]||tr,o===tr)return;if(u=o[0],a=p[o[1]],a&&(n=o[2],n=void 0===n?i:n,a()===!1))return;if(u===Kn)return l.raw=t,l}}function Nt(t){var e=Vn.get(t);return e||(e=Tt(t),e&&Vn.put(t,e)),e}function jt(t,e){return It(e).get(t)}function Et(e,i,n){var r=e;if("string"==typeof i&&(i=Tt(i)),!i||!m(e))return!1;for(var s,o,a=0,h=i.length;h>a;a++)s=e,o=i[a],"*"===o.charAt(0)&&(o=It(o.slice(1)).get.call(r,r)),h-1>a?(e=e[o],m(e)||(e={},t(s,o,e))):Di(e)?e.$set(o,n):o in e?e[o]=n:t(e,o,n);return!0}function St(){}function Ft(t,e){var i=vr.length;return vr[i]=e?t.replace(lr,"\\n"):t,'"'+i+'"'}function Dt(t){var e=t.charAt(0),i=t.slice(1);return sr.test(i)?t:(i=i.indexOf('"')>-1?i.replace(ur,Pt):i,e+"scope."+i)}function Pt(t,e){return vr[e]}function Rt(t){ar.test(t),vr.length=0;var e=t.replace(cr,Ft).replace(hr,"");return e=(" "+e).replace(pr,Dt).replace(ur,Pt),Lt(e)}function Lt(t){try{return new Function("scope","return "+t+";")}catch(e){return St}}function Ht(t){var e=Nt(t);return e?function(t,i){Et(t,e,i)}:void 0}function It(t,e){t=t.trim();var i=nr.get(t);if(i)return e&&!i.set&&(i.set=Ht(i.exp)),i;var n={exp:t};return n.get=Mt(t)&&t.indexOf("[")<0?Lt("scope."+t):Rt(t),e&&(n.set=Ht(t)),nr.put(t,n),n}function Mt(t){return fr.test(t)&&!dr.test(t)&&"Math."!==t.slice(0,5)}function Vt(){gr.length=0,_r.length=0,yr={},br={},wr=!1}function Bt(){for(var t=!0;t;)t=!1,Wt(gr),Wt(_r),gr.length?t=!0:(Li&&An.devtools&&Li.emit("flush"),Vt())}function Wt(t){for(var e=0;e<t.length;e++){var i=t[e],n=i.id;yr[n]=null,i.run()}t.length=0}function zt(t){var e=t.id;if(null==yr[e]){var i=t.user?_r:gr;yr[e]=i.length,i.push(t),wr||(wr=!0,Yi(Bt))}}function Ut(t,e,i,n){n&&v(this,n);var r="function"==typeof e;if(this.vm=t,t._watchers.push(this),this.expression=e,this.cb=i,this.id=++Cr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new Ki,this.newDepIds=new Ki,this.prevError=null,r)this.getter=e,this.setter=void 0;else{var s=It(e,this.twoWay);this.getter=s.get,this.setter=s.set}this.value=this.lazy?void 0:this.get(),this.queued=this.shallow=!1}function Jt(t,e){var i=void 0,n=void 0;e||(e=$r,e.clear());var r=Di(t),s=m(t);if((r||s)&&Object.isExtensible(t)){if(t.__ob__){var o=t.__ob__.dep.id;if(e.has(o))return;e.add(o)}if(r)for(i=t.length;i--;)Jt(t[i],e);else if(s)for(n=Object.keys(t),i=n.length;i--;)Jt(t[n[i]],e)}}function qt(t){return it(t)&&at(t.content)}function Qt(t,e){var i=e?t:t.trim(),n=xr.get(i);if(n)return n;var r=document.createDocumentFragment(),s=t.match(Tr),o=Nr.test(t),a=jr.test(t);if(s||o||a){var h=s&&s[1],l=Or[h]||Or.efault,c=l[0],u=l[1],f=l[2],p=document.createElement("div");for(p.innerHTML=u+t+f;c--;)p=p.lastChild;for(var d;d=p.firstChild;)r.appendChild(d)}else r.appendChild(document.createTextNode(t));return e||tt(r),xr.put(i,r),r}function Gt(t){if(qt(t))return Qt(t.innerHTML);if("SCRIPT"===t.tagName)return Qt(t.textContent);for(var e,i=Zt(t),n=document.createDocumentFragment();e=i.firstChild;)n.appendChild(e);return tt(n),n}function Zt(t){if(!t.querySelectorAll)return t.cloneNode();var e,i,n,r=t.cloneNode(!0);if(Er){var s=r;if(qt(t)&&(t=t.content,s=r.content),i=t.querySelectorAll("template"),i.length)for(n=s.querySelectorAll("template"),e=n.length;e--;)n[e].parentNode.replaceChild(Zt(i[e]),n[e])}if(Sr)if("TEXTAREA"===t.tagName)r.value=t.value;else if(i=t.querySelectorAll("textarea"),i.length)for(n=r.querySelectorAll("textarea"),e=n.length;e--;)n[e].value=i[e].value;return r}function Xt(t,e,i){var n,r;return at(t)?(tt(t),e?Zt(t):t):("string"==typeof t?i||"#"!==t.charAt(0)?r=Qt(t,i):(r=Ar.get(t),r||(n=document.getElementById(t.slice(1)),n&&(r=Gt(n),Ar.put(t,r)))):t.nodeType&&(r=Gt(t)),r&&e?Zt(r):r)}function Yt(t,e,i,n,r,s){this.children=[],this.childFrags=[],this.vm=e,this.scope=r,this.inserted=!1,this.parentFrag=s,s&&s.childFrags.push(this),this.unlink=t(e,i,n,r,this);var o=this.single=1===i.childNodes.length&&!i.childNodes[0].__v_anchor;o?(this.node=i.childNodes[0],this.before=Kt,this.remove=te):(this.node=nt("fragment-start"),this.end=nt("fragment-end"),this.frag=i,U(this.node,i),i.appendChild(this.end),this.before=ee,this.remove=ie),this.node.__v_frag=this}function Kt(t,e){this.inserted=!0;var i=e!==!1?D:B;i(this.node,t,this.vm),H(this.node)&&this.callHook(ne)}function te(){this.inserted=!1;var t=H(this.node),e=this;this.beforeRemove(),P(this.node,this.vm,function(){t&&e.callHook(re),e.destroy()})}function ee(t,e){this.inserted=!0;var i=this.vm,n=e!==!1?D:B;st(this.node,this.end,function(e){n(e,t,i)}),H(this.node)&&this.callHook(ne)}function ie(){this.inserted=!1;var t=this,e=H(this.node);this.beforeRemove(),ot(this.node,this.end,this.vm,this.frag,function(){e&&t.callHook(re),t.destroy()})}function ne(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function re(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}function se(t,e){this.vm=t;var i,n="string"==typeof e;n||it(e)&&!e.hasAttribute("v-if")?i=Xt(e,!0):(i=document.createDocumentFragment(),i.appendChild(e)),this.template=i;var r,s=t.constructor.cid;if(s>0){var o=s+(n?e:ht(e));r=Pr.get(o),r||(r=De(i,t.$options,!0),Pr.put(o,r))}else r=De(i,t.$options,!0);this.linker=r}function oe(t,e,i){var n=t.node.previousSibling;if(n){for(t=n.__v_frag;!(t&&t.forId===i&&t.inserted||n===e);){if(n=n.previousSibling,!n)return;t=n.__v_frag}return t}}function ae(t){var e=t.node;if(t.end)for(;!e.__vue__&&e!==t.end&&e.nextSibling;)e=e.nextSibling;return e.__vue__}function he(t){for(var e=-1,i=new Array(Math.floor(t));++e<t;)i[e]=e;return i}function le(t,e,i,n){return n?"$index"===n?t:n.charAt(0).match(/\w/)?jt(i,n):i[n]:e||i}function ce(t,e,i){for(var n,r,s,o=e?[]:null,a=0,h=t.options.length;h>a;a++)if(n=t.options[a],s=i?n.hasAttribute("selected"):n.selected){if(r=n.hasOwnProperty("_value")?n._value:n.value,!e)return r;o.push(r)}return o}function ue(t,e){for(var i=t.length;i--;)if(C(t[i],e))return i;return-1}function fe(t,e){var i=e.map(function(t){var e=t.charCodeAt(0);return e>47&&58>e?parseInt(t,10):1===t.length&&(e=t.toUpperCase().charCodeAt(0),e>64&&91>e)?e:is[t]});return i=[].concat.apply([],i),function(e){return i.indexOf(e.keyCode)>-1?t.call(this,e):void 0}}function pe(t){return function(e){return e.stopPropagation(),t.call(this,e)}}function de(t){return function(e){return e.preventDefault(),t.call(this,e)}}function ve(t){return function(e){return e.target===e.currentTarget?t.call(this,e):void 0}}function me(t){if(as[t])return as[t];var e=ge(t);return as[t]=as[e]=e,e}function ge(t){t=u(t);var e=l(t),i=e.charAt(0).toUpperCase()+e.slice(1);hs||(hs=document.createElement("div"));var n,r=rs.length;if("filter"!==e&&e in hs.style)return{kebab:t,camel:e};for(;r--;)if(n=ss[r]+i,n in hs.style)return{kebab:rs[r]+t,camel:n}}function _e(t){var e=[];if(Di(t))for(var i=0,n=t.length;n>i;i++){var r=t[i];if(r)if("string"==typeof r)e.push(r);else for(var s in r)r[s]&&e.push(s)}else if(m(t))for(var o in t)t[o]&&e.push(o);return e}function ye(t,e,i){if(e=e.trim(),-1===e.indexOf(" "))return void i(t,e);for(var n=e.split(/\s+/),r=0,s=n.length;s>r;r++)i(t,n[r])}function be(t,e,i){function n(){++s>=r?i():t[s].call(e,n)}var r=t.length,s=0;t[0].call(e,n)}function we(t,e,i){for(var r,s,o,a,h,c,f,p=[],d=Object.keys(e),v=d.length;v--;)s=d[v],r=e[s]||ks,h=l(s),xs.test(h)&&(f={name:s,path:h,options:r,mode:$s.ONE_WAY,raw:null},o=u(s),null===(a=M(t,o))&&(null!==(a=M(t,o+".sync"))?f.mode=$s.TWO_WAY:null!==(a=M(t,o+".once"))&&(f.mode=$s.ONE_TIME)),null!==a?(f.raw=a,c=A(a),a=c.expression,f.filters=c.filters,n(a)&&!c.filters?f.optimizedLiteral=!0:f.dynamic=!0,f.parentPath=a):null!==(a=I(t,o))&&(f.raw=a),p.push(f));return Ce(p)}function Ce(t){return function(e,n){e._props={};for(var r,s,l,c,f,p=e.$options.propsData,d=t.length;d--;)if(r=t[d],f=r.raw,s=r.path,l=r.options,e._props[s]=r,p&&i(p,s)&&ke(e,r,p[s]),null===f)ke(e,r,void 0);else if(r.dynamic)r.mode===$s.ONE_TIME?(c=(n||e._context||e).$get(r.parentPath),ke(e,r,c)):e._context?e._bindDir({name:"prop",def:Os,prop:r},null,null,n):ke(e,r,e.$get(r.parentPath));else if(r.optimizedLiteral){var v=h(f);c=v===f?a(o(f)):v,ke(e,r,c)}else c=l.type!==Boolean||""!==f&&f!==u(r.name)?f:!0,ke(e,r,c)}}function $e(t,e,i,n){var r=e.dynamic&&Mt(e.parentPath),s=i;void 0===s&&(s=Ae(t,e)),s=Te(e,s,t);var o=s!==i;Oe(e,s,t)||(s=void 0),r&&!o?yt(function(){n(s)}):n(s)}function ke(t,e,i){$e(t,e,i,function(i){kt(t,e.path,i)})}function xe(t,e,i){$e(t,e,i,function(i){t[e.path]=i})}function Ae(t,e){var n=e.options;if(!i(n,"default"))return n.type===Boolean?!1:void 0;var r=n["default"];return m(r),"function"==typeof r&&n.type!==Function?r.call(t):r}function Oe(t,e,i){if(!t.options.required&&(null===t.raw||null==e))return!0;var n=t.options,r=n.type,s=!r,o=[];if(r){Di(r)||(r=[r]);for(var a=0;a<r.length&&!s;a++){var h=Ne(e,r[a]);o.push(h.expectedType),s=h.valid}}if(!s)return!1;var l=n.validator;return!l||l(e)}function Te(t,e,i){var n=t.options.coerce;return n&&"function"==typeof n?n(e):e}function Ne(t,e){var i,n;return e===String?(n="string",i=typeof t===n):e===Number?(n="number",i=typeof t===n):e===Boolean?(n="boolean",i=typeof t===n):e===Function?(n="function",i=typeof t===n):e===Object?(n="object",i=g(t)):e===Array?(n="array",i=Di(t)):i=t instanceof e,{valid:i,expectedType:n}}function je(t){Ts.push(t),Ns||(Ns=!0,Yi(Ee))}function Ee(){for(var t=document.documentElement.offsetHeight,e=0;e<Ts.length;e++)Ts[e]();return Ts=[],Ns=!1,t}function Se(t,e,i,n){this.id=e,this.el=t,this.enterClass=i&&i.enterClass||e+"-enter",this.leaveClass=i&&i.leaveClass||e+"-leave",this.hooks=i,this.vm=n,this.pendingCssEvent=this.pendingCssCb=this.cancel=this.pendingJsCb=this.op=this.cb=null,this.justEntered=!1,this.entered=this.left=!1,this.typeCache={},this.type=i&&i.type;var r=this;["enterNextTick","enterDone","leaveNextTick","leaveDone"].forEach(function(t){r[t]=p(r[t],r)})}function Fe(t){if(/svg$/.test(t.namespaceURI)){var e=t.getBoundingClientRect();return!(e.width||e.height)}return!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function De(t,e,i){var n=i||!e._asComponent?Ve(t,e):null,r=n&&n.terminal||ri(t)||!t.hasChildNodes()?null:qe(t.childNodes,e);return function(t,e,i,s,o){var a=d(e.childNodes),h=Pe(function(){n&&n(t,e,i,s,o),r&&r(t,a,i,s,o)},t);return Le(t,h)}}function Pe(t,e){e._directives=[];var i=e._directives.length;t();var n=e._directives.slice(i);n.sort(Re);for(var r=0,s=n.length;s>r;r++)n[r]._bind();return n}function Re(t,e){return t=t.descriptor.def.priority||zs,e=e.descriptor.def.priority||zs,t>e?-1:t===e?0:1}function Le(t,e,i,n){function r(r){He(t,e,r),i&&n&&He(i,n)}return r.dirs=e,r}function He(t,e,i){for(var n=e.length;n--;)e[n]._teardown()}function Ie(t,e,i,n){var r=we(e,i,t),s=Pe(function(){r(t,n)},t);return Le(t,s)}function Me(t,e,i){var n,r,s=e._containerAttrs,o=e._replacerAttrs;return 11!==t.nodeType&&(e._asComponent?(s&&i&&(n=ti(s,i)),o&&(r=ti(o,e))):r=ti(t.attributes,e)),e._containerAttrs=e._replacerAttrs=null,function(t,e,i){var s,o=t._context;o&&n&&(s=Pe(function(){n(o,e,null,i)},o));var a=Pe(function(){r&&r(t,e)},t);return Le(t,a,o,s)}}function Ve(t,e){var i=t.nodeType;return 1!==i||ri(t)?3===i&&t.data.trim()?We(t,e):null:Be(t,e)}function Be(t,e){if("TEXTAREA"===t.tagName){var i=N(t.value);i&&(t.setAttribute(":value",j(i)),t.value="")}var n,r=t.hasAttributes(),s=r&&d(t.attributes);return r&&(n=Xe(t,s,e)),n||(n=Ge(t,e)),n||(n=Ze(t,e)),!n&&r&&(n=ti(s,e)),n}function We(t,e){if(t._skip)return ze;var i=N(t.wholeText);if(!i)return null;for(var n=t.nextSibling;n&&3===n.nodeType;)n._skip=!0,n=n.nextSibling;for(var r,s,o=document.createDocumentFragment(),a=0,h=i.length;h>a;a++)s=i[a],r=s.tag?Ue(s,e):document.createTextNode(s.value),o.appendChild(r);return Je(i,o,e)}function ze(t,e){z(e)}function Ue(t,e){function i(e){if(!t.descriptor){var i=A(t.value);t.descriptor={name:e,def:bs[e],expression:i.expression,filters:i.filters}}}var n;return t.oneTime?n=document.createTextNode(t.value):t.html?(n=document.createComment("v-html"),i("html")):(n=document.createTextNode(" "),i("text")),n}function Je(t,e){return function(i,n,r,o){for(var a,h,l,c=e.cloneNode(!0),u=d(c.childNodes),f=0,p=t.length;p>f;f++)a=t[f],h=a.value,a.tag&&(l=u[f],a.oneTime?(h=(o||i).$eval(h),a.html?J(l,Xt(h,!0)):l.data=s(h)):i._bindDir(a.descriptor,l,r,o));J(n,c)}}function qe(t,e){for(var i,n,r,s=[],o=0,a=t.length;a>o;o++)r=t[o],i=Ve(r,e),n=i&&i.terminal||"SCRIPT"===r.tagName||!r.hasChildNodes()?null:qe(r.childNodes,e),s.push(i,n);return s.length?Qe(s):null}function Qe(t){return function(e,i,n,r,s){for(var o,a,h,l=0,c=0,u=t.length;u>l;c++){o=i[c],a=t[l++],h=t[l++];var f=d(o.childNodes);a&&a(e,o,n,r,s),h&&h(e,f,n,r,s)}}}function Ge(t,e){var i=t.tagName.toLowerCase();if(!jn.test(i)){var n=gt(e,"elementDirectives",i);return n?Ke(t,i,"",e,n):void 0}}function Ze(t,e){var i=lt(t,e);if(i){var n=rt(t),r={name:"component",ref:n,expression:i.id,def:Hs.component,modifiers:{literal:!i.dynamic}},s=function(t,e,i,s,o){n&&kt((s||t).$refs,n,null),t._bindDir(r,e,i,s,o)};return s.terminal=!0,s}}function Xe(t,e,i){if(null!==I(t,"v-pre"))return Ye;if(t.hasAttribute("v-else")){var n=t.previousElementSibling;if(n&&n.hasAttribute("v-if"))return Ye}for(var r,s,o,a,h,l,c,u,f,p,d=0,v=e.length;v>d;d++)r=e[d],s=r.name.replace(Bs,""),(h=s.match(Vs))&&(f=gt(i,"directives",h[1]),f&&f.terminal&&(!p||(f.priority||Us)>p.priority)&&(p=f,c=r.name,a=ei(r.name),o=r.value,l=h[1],u=h[2]));return p?Ke(t,l,o,i,p,c,u,a):void 0}function Ye(){}function Ke(t,e,i,n,r,s,o,a){var h=A(i),l={name:e,arg:o,expression:h.expression,filters:h.filters,raw:i,attr:s,modifiers:a,def:r};"for"!==e&&"router-view"!==e||(l.ref=rt(t));var c=function(t,e,i,n,r){l.ref&&kt((n||t).$refs,l.ref,null),t._bindDir(l,e,i,n,r)};return c.terminal=!0,c}function ti(t,e){function i(t,e,i){var n=i&&ni(i),r=!n&&A(s);v.push({name:t,attr:o,raw:a,def:e,arg:l,modifiers:c,expression:r&&r.expression,filters:r&&r.filters,interp:i,hasOneTime:n})}for(var n,r,s,o,a,h,l,c,u,f,p,d=t.length,v=[];d--;)if(n=t[d],r=o=n.name,s=a=n.value,f=N(s),l=null,c=ei(r),r=r.replace(Bs,""),f)s=j(f),l=r,i("bind",bs.bind,f);else if(Ws.test(r))c.literal=!Is.test(r),i("transition",Hs.transition);else if(Ms.test(r))l=r.replace(Ms,""),i("on",bs.on);else if(Is.test(r))h=r.replace(Is,""),"style"===h||"class"===h?i(h,Hs[h]):(l=h,i("bind",bs.bind));else if(p=r.match(Vs)){if(h=p[1],l=p[2],"else"===h)continue;u=gt(e,"directives",h,!0),u&&i(h,u)}return v.length?ii(v):void 0}function ei(t){var e=Object.create(null),i=t.match(Bs);if(i)for(var n=i.length;n--;)e[i[n].slice(1)]=!0;return e}function ii(t){return function(e,i,n,r,s){for(var o=t.length;o--;)e._bindDir(t[o],i,n,r,s)}}function ni(t){for(var e=t.length;e--;)if(t[e].oneTime)return!0}function ri(t){return"SCRIPT"===t.tagName&&(!t.hasAttribute("type")||"text/javascript"===t.getAttribute("type"))}function si(t,e){return e&&(e._containerAttrs=ai(t)),it(t)&&(t=Xt(t)),e&&(e._asComponent&&!e.template&&(e.template="<slot></slot>"),e.template&&(e._content=K(t),t=oi(t,e))),at(t)&&(U(nt("v-start",!0),t),t.appendChild(nt("v-end",!0))),t}function oi(t,e){var i=e.template,n=Xt(i,!0);if(n){var r=n.firstChild,s=r.tagName&&r.tagName.toLowerCase();return e.replace?(t===document.body,n.childNodes.length>1||1!==r.nodeType||"component"===s||gt(e,"components",s)||V(r,"is")||gt(e,"elementDirectives",s)||r.hasAttribute("v-for")||r.hasAttribute("v-if")?n:(e._replacerAttrs=ai(r),hi(t,r),r)):(t.appendChild(n),t)}}function ai(t){return 1===t.nodeType&&t.hasAttributes()?d(t.attributes):void 0}function hi(t,e){for(var i,n,r=t.attributes,s=r.length;s--;)i=r[s].name,n=r[s].value,e.hasAttribute(i)||Js.test(i)?"class"===i&&!N(n)&&(n=n.trim())&&n.split(/\s+/).forEach(function(t){X(e,t)}):e.setAttribute(i,n)}function li(t,e){if(e){for(var i,n,r=t._slotContents=Object.create(null),s=0,o=e.children.length;o>s;s++)i=e.children[s],(n=i.getAttribute("slot"))&&(r[n]||(r[n]=[])).push(i);for(n in r)r[n]=ci(r[n],e);if(e.hasChildNodes()){var a=e.childNodes;if(1===a.length&&3===a[0].nodeType&&!a[0].data.trim())return;r["default"]=ci(e.childNodes,e)}}}function ci(t,e){var i=document.createDocumentFragment();t=d(t);for(var n=0,r=t.length;r>n;n++){var s=t[n];!it(s)||s.hasAttribute("v-if")||s.hasAttribute("v-for")||(e.removeChild(s),s=Xt(s,!0)),i.appendChild(s)}return i}function ui(t){function e(){}function n(t,e){var i=new Ut(e,t,null,{lazy:!0});return function(){return i.dirty&&i.evaluate(),_t.target&&i.depend(),i.value}}Object.defineProperty(t.prototype,"$data",{get:function(){return this._data},set:function(t){t!==this._data&&this._setData(t)}}),t.prototype._initState=function(){this._initProps(),this._initMeta(),this._initMethods(),this._initData(),this._initComputed()},t.prototype._initProps=function(){var t=this.$options,e=t.el,i=t.props;e=t.el=L(e),this._propsUnlinkFn=e&&1===e.nodeType&&i?Ie(this,e,i,this._scope):null},t.prototype._initData=function(){var t=this.$options.data,e=this._data=t?t():{};g(e)||(e={});var n,r,s=this._props,o=Object.keys(e);for(n=o.length;n--;)r=o[n],s&&i(s,r)||this._proxy(r);$t(e,this)},t.prototype._setData=function(t){t=t||{};var e=this._data;this._data=t;var n,r,s;for(n=Object.keys(e),s=n.length;s--;)r=n[s],r in t||this._unproxy(r);for(n=Object.keys(t),s=n.length;s--;)r=n[s],i(this,r)||this._proxy(r);e.__ob__.removeVm(this),$t(t,this),this._digest()},t.prototype._proxy=function(t){if(!r(t)){var e=this;Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(i){e._data[t]=i}})}},t.prototype._unproxy=function(t){r(t)||delete this[t]},t.prototype._digest=function(){for(var t=0,e=this._watchers.length;e>t;t++)this._watchers[t].update(!0)},t.prototype._initComputed=function(){var t=this.$options.computed;if(t)for(var i in t){var r=t[i],s={enumerable:!0,configurable:!0};"function"==typeof r?(s.get=n(r,this),s.set=e):(s.get=r.get?r.cache!==!1?n(r.get,this):p(r.get,this):e,s.set=r.set?p(r.set,this):e),Object.defineProperty(this,i,s)}},t.prototype._initMethods=function(){var t=this.$options.methods;if(t)for(var e in t)this[e]=p(t[e],this)},t.prototype._initMeta=function(){var t=this.$options._meta;if(t)for(var e in t)kt(this,e,t[e])}}function fi(t){function e(t,e){for(var i,n,r,s=e.attributes,o=0,a=s.length;a>o;o++)i=s[o].name,Qs.test(i)&&(i=i.replace(Qs,""),n=s[o].value,Mt(n)&&(n+=".apply(this, $arguments)"),r=(t._scope||t._context).$eval(n,!0),r._fromParent=!0,t.$on(i.replace(Qs),r))}function i(t,e,i){if(i){var r,s,o,a;for(s in i)if(r=i[s],Di(r))for(o=0,a=r.length;a>o;o++)n(t,e,s,r[o]);else n(t,e,s,r)}}function n(t,e,i,r,s){var o=typeof r;if("function"===o)t[e](i,r,s);else if("string"===o){var a=t.$options.methods,h=a&&a[r];h&&t[e](i,h,s)}else r&&"object"===o&&n(t,e,i,r.handler,r)}function r(){this._isAttached||(this._isAttached=!0,this.$children.forEach(s))}function s(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function o(){this._isAttached&&(this._isAttached=!1,this.$children.forEach(a))}function a(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}t.prototype._initEvents=function(){var t=this.$options;t._asComponent&&e(this,t.el),i(this,"$on",t.events),i(this,"$watch",t.watch)},t.prototype._initDOMHooks=function(){this.$on("hook:attached",r),this.$on("hook:detached",o)},t.prototype._callHook=function(t){this.$emit("pre-hook:"+t);var e=this.$options[t];if(e)for(var i=0,n=e.length;n>i;i++)e[i].call(this);this.$emit("hook:"+t)}}function pi(){}function di(t,e,i,n,r,s){this.vm=e,this.el=i,this.descriptor=t,this.name=t.name,this.expression=t.expression,this.arg=t.arg,this.modifiers=t.modifiers,this.filters=t.filters,this.literal=this.modifiers&&this.modifiers.literal,this._locked=!1,this._bound=!1,this._listeners=null,this._host=n,this._scope=r,this._frag=s}function vi(t){t.prototype._updateRef=function(t){var e=this.$options._ref;if(e){var i=(this._scope||this._context).$refs;t?i[e]===this&&(i[e]=null):i[e]=this}},t.prototype._compile=function(t){var e=this.$options,i=t;if(t=si(t,e),this._initElement(t),1!==t.nodeType||null===I(t,"v-pre")){var n=this._context&&this._context.$options,r=Me(t,e,n);li(this,e._content);var s,o=this.constructor;e._linkerCachable&&(s=o.linker,s||(s=o.linker=De(t,e)));var a=r(this,t,this._scope),h=s?s(this,t):De(t,e)(this,t);this._unlinkFn=function(){a(),h(!0)},e.replace&&J(i,t),this._isCompiled=!0,this._callHook("compiled")}},t.prototype._initElement=function(t){at(t)?(this._isFragment=!0,this.$el=this._fragmentStart=t.firstChild,this._fragmentEnd=t.lastChild,3===this._fragmentStart.nodeType&&(this._fragmentStart.data=this._fragmentEnd.data=""),this._fragment=t):this.$el=t,this.$el.__vue__=this,this._callHook("beforeCompile")},t.prototype._bindDir=function(t,e,i,n,r){this._directives.push(new di(t,this,e,i,n,r))},t.prototype._destroy=function(t,e){if(this._isBeingDestroyed)return void(e||this._cleanup());var i,n,r=this,s=function(){!i||n||e||r._cleanup()};t&&this.$el&&(n=!0,this.$remove(function(){
+n=!1,s()})),this._callHook("beforeDestroy"),this._isBeingDestroyed=!0;var o,a=this.$parent;for(a&&!a._isBeingDestroyed&&(a.$children.$remove(this),this._updateRef(!0)),o=this.$children.length;o--;)this.$children[o].$destroy();for(this._propsUnlinkFn&&this._propsUnlinkFn(),this._unlinkFn&&this._unlinkFn(),o=this._watchers.length;o--;)this._watchers[o].teardown();this.$el&&(this.$el.__vue__=null),i=!0,s()},t.prototype._cleanup=function(){this._isDestroyed||(this._frag&&this._frag.children.$remove(this),this._data&&this._data.__ob__&&this._data.__ob__.removeVm(this),this.$el=this.$parent=this.$root=this.$children=this._watchers=this._context=this._scope=this._directives=null,this._isDestroyed=!0,this._callHook("destroyed"),this.$off())}}function mi(t){t.prototype._applyFilters=function(t,e,i,n){var r,s,o,a,h,l,c,u,f;for(l=0,c=i.length;c>l;l++)if(r=i[n?c-l-1:l],s=gt(this.$options,"filters",r.name,!0),s&&(s=n?s.write:s.read||s,"function"==typeof s)){if(o=n?[t,e]:[t],h=n?2:1,r.args)for(u=0,f=r.args.length;f>u;u++)a=r.args[u],o[u+h]=a.dynamic?this.$get(a.value):a.value;t=s.apply(this,o)}return t},t.prototype._resolveComponent=function(e,i){var n;if(n="function"==typeof e?e:gt(this.$options,"components",e,!0))if(n.options)i(n);else if(n.resolved)i(n.resolved);else if(n.requested)n.pendingCallbacks.push(i);else{n.requested=!0;var r=n.pendingCallbacks=[i];n.call(this,function(e){g(e)&&(e=t.extend(e)),n.resolved=e;for(var i=0,s=r.length;s>i;i++)r[i](e)},function(t){})}}}function gi(t){function i(t){return JSON.parse(JSON.stringify(t))}t.prototype.$get=function(t,e){var i=It(t);if(i){if(e){var n=this;return function(){n.$arguments=d(arguments);var t=i.get.call(n,n);return n.$arguments=null,t}}try{return i.get.call(this,this)}catch(r){}}},t.prototype.$set=function(t,e){var i=It(t,!0);i&&i.set&&i.set.call(this,this,e)},t.prototype.$delete=function(t){e(this._data,t)},t.prototype.$watch=function(t,e,i){var n,r=this;"string"==typeof t&&(n=A(t),t=n.expression);var s=new Ut(r,t,e,{deep:i&&i.deep,sync:i&&i.sync,filters:n&&n.filters,user:!i||i.user!==!1});return i&&i.immediate&&e.call(r,s.value),function(){s.teardown()}},t.prototype.$eval=function(t,e){if(Gs.test(t)){var i=A(t),n=this.$get(i.expression,e);return i.filters?this._applyFilters(n,null,i.filters):n}return this.$get(t,e)},t.prototype.$interpolate=function(t){var e=N(t),i=this;return e?1===e.length?i.$eval(e[0].value)+"":e.map(function(t){return t.tag?i.$eval(t.value):t.value}).join(""):t},t.prototype.$log=function(t){var e=t?jt(this._data,t):this._data;if(e&&(e=i(e)),!t){var n;for(n in this.$options.computed)e[n]=i(this[n]);if(this._props)for(n in this._props)e[n]=i(this[n])}console.log(e)}}function _i(t){function e(t,e,n,r,s,o){e=i(e);var a=!H(e),h=r===!1||a?s:o,l=!a&&!t._isAttached&&!H(t.$el);return t._isFragment?(st(t._fragmentStart,t._fragmentEnd,function(i){h(i,e,t)}),n&&n()):h(t.$el,e,t,n),l&&t._callHook("attached"),t}function i(t){return"string"==typeof t?document.querySelector(t):t}function n(t,e,i,n){e.appendChild(t),n&&n()}function r(t,e,i,n){B(t,e),n&&n()}function s(t,e,i){z(t),i&&i()}t.prototype.$nextTick=function(t){Yi(t,this)},t.prototype.$appendTo=function(t,i,r){return e(this,t,i,r,n,F)},t.prototype.$prependTo=function(t,e,n){return t=i(t),t.hasChildNodes()?this.$before(t.firstChild,e,n):this.$appendTo(t,e,n),this},t.prototype.$before=function(t,i,n){return e(this,t,i,n,r,D)},t.prototype.$after=function(t,e,n){return t=i(t),t.nextSibling?this.$before(t.nextSibling,e,n):this.$appendTo(t.parentNode,e,n),this},t.prototype.$remove=function(t,e){if(!this.$el.parentNode)return t&&t();var i=this._isAttached&&H(this.$el);i||(e=!1);var n=this,r=function(){i&&n._callHook("detached"),t&&t()};if(this._isFragment)ot(this._fragmentStart,this._fragmentEnd,this,this._fragment,r);else{var o=e===!1?s:P;o(this.$el,this,r)}return this}}function yi(t){function e(t,e,n){var r=t.$parent;if(r&&n&&!i.test(e))for(;r;)r._eventsCount[e]=(r._eventsCount[e]||0)+n,r=r.$parent}t.prototype.$on=function(t,i){return(this._events[t]||(this._events[t]=[])).push(i),e(this,t,1),this},t.prototype.$once=function(t,e){function i(){n.$off(t,i),e.apply(this,arguments)}var n=this;return i.fn=e,this.$on(t,i),this},t.prototype.$off=function(t,i){var n;if(!arguments.length){if(this.$parent)for(t in this._events)n=this._events[t],n&&e(this,t,-n.length);return this._events={},this}if(n=this._events[t],!n)return this;if(1===arguments.length)return e(this,t,-n.length),this._events[t]=null,this;for(var r,s=n.length;s--;)if(r=n[s],r===i||r.fn===i){e(this,t,-1),n.splice(s,1);break}return this},t.prototype.$emit=function(t){var e="string"==typeof t;t=e?t:t.name;var i=this._events[t],n=e||!i;if(i){i=i.length>1?d(i):i;var r=e&&i.some(function(t){return t._fromParent});r&&(n=!1);for(var s=d(arguments,1),o=0,a=i.length;a>o;o++){var h=i[o],l=h.apply(this,s);l!==!0||r&&!h._fromParent||(n=!0)}}return n},t.prototype.$broadcast=function(t){var e="string"==typeof t;if(t=e?t:t.name,this._eventsCount[t]){var i=this.$children,n=d(arguments);e&&(n[0]={name:t,source:this});for(var r=0,s=i.length;s>r;r++){var o=i[r],a=o.$emit.apply(o,n);a&&o.$broadcast.apply(o,n)}return this}},t.prototype.$dispatch=function(t){var e=this.$emit.apply(this,arguments);if(e){var i=this.$parent,n=d(arguments);for(n[0]={name:t,source:this};i;)e=i.$emit.apply(i,n),i=e?i.$parent:null;return this}};var i=/^hook:/}function bi(t){function e(){this._isAttached=!0,this._isReady=!0,this._callHook("ready")}t.prototype.$mount=function(t){return this._isCompiled?void 0:(t=L(t),t||(t=document.createElement("div")),this._compile(t),this._initDOMHooks(),H(this.$el)?(this._callHook("attached"),e.call(this)):this.$once("hook:attached",e),this)},t.prototype.$destroy=function(t,e){this._destroy(t,e)},t.prototype.$compile=function(t,e,i,n){return De(t,this.$options,!0)(this,t,e,i,n)}}function wi(t){this._init(t)}function Ci(t,e,i){return i=i?parseInt(i,10):0,e=o(e),"number"==typeof e?t.slice(i,i+e):t}function $i(t,e,i){if(t=Ks(t),null==e)return t;if("function"==typeof e)return t.filter(e);e=(""+e).toLowerCase();for(var n,r,s,o,a="in"===i?3:2,h=Array.prototype.concat.apply([],d(arguments,a)),l=[],c=0,u=t.length;u>c;c++)if(n=t[c],s=n&&n.$value||n,o=h.length){for(;o--;)if(r=h[o],"$key"===r&&xi(n.$key,e)||xi(jt(s,r),e)){l.push(n);break}}else xi(n,e)&&l.push(n);return l}function ki(t){function e(t,e,i){var r=n[i];return r&&("$key"!==r&&(m(t)&&"$value"in t&&(t=t.$value),m(e)&&"$value"in e&&(e=e.$value)),t=m(t)?jt(t,r):t,e=m(e)?jt(e,r):e),t===e?0:t>e?s:-s}var i=null,n=void 0;t=Ks(t);var r=d(arguments,1),s=r[r.length-1];"number"==typeof s?(s=0>s?-1:1,r=r.length>1?r.slice(0,-1):r):s=1;var o=r[0];return o?("function"==typeof o?i=function(t,e){return o(t,e)*s}:(n=Array.prototype.concat.apply([],r),i=function(t,r,s){return s=s||0,s>=n.length-1?e(t,r,s):e(t,r,s)||i(t,r,s+1)}),t.slice().sort(i)):t}function xi(t,e){var i;if(g(t)){var n=Object.keys(t);for(i=n.length;i--;)if(xi(t[n[i]],e))return!0}else if(Di(t)){for(i=t.length;i--;)if(xi(t[i],e))return!0}else if(null!=t)return t.toString().toLowerCase().indexOf(e)>-1}function Ai(i){function n(t){return new Function("return function "+f(t)+" (options) { this._init(options) }")()}i.options={directives:bs,elementDirectives:Ys,filters:eo,transitions:{},components:{},partials:{},replace:!0},i.util=In,i.config=An,i.set=t,i["delete"]=e,i.nextTick=Yi,i.compiler=qs,i.FragmentFactory=se,i.internalDirectives=Hs,i.parsers={path:ir,text:$n,template:Fr,directive:gn,expression:mr},i.cid=0;var r=1;i.extend=function(t){t=t||{};var e=this,i=0===e.cid;if(i&&t._Ctor)return t._Ctor;var s=t.name||e.options.name,o=n(s||"VueComponent");return o.prototype=Object.create(e.prototype),o.prototype.constructor=o,o.cid=r++,o.options=mt(e.options,t),o["super"]=e,o.extend=e.extend,An._assetTypes.forEach(function(t){o[t]=e[t]}),s&&(o.options.components[s]=o),i&&(t._Ctor=o),o},i.use=function(t){if(!t.installed){var e=d(arguments,1);return e.unshift(this),"function"==typeof t.install?t.install.apply(t,e):t.apply(null,e),t.installed=!0,this}},i.mixin=function(t){i.options=mt(i.options,t)},An._assetTypes.forEach(function(t){i[t]=function(e,n){return n?("component"===t&&g(n)&&(n.name||(n.name=e),n=i.extend(n)),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}),v(i.transition,Tn)}var Oi=Object.prototype.hasOwnProperty,Ti=/^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/,Ni=/-(\w)/g,ji=/([a-z\d])([A-Z])/g,Ei=/(?:^|[-_\/])(\w)/g,Si=Object.prototype.toString,Fi="[object Object]",Di=Array.isArray,Pi="__proto__"in{},Ri="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Li=Ri&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Hi=Ri&&window.navigator.userAgent.toLowerCase(),Ii=Hi&&Hi.indexOf("trident")>0,Mi=Hi&&Hi.indexOf("msie 9.0")>0,Vi=Hi&&Hi.indexOf("android")>0,Bi=Hi&&/(iphone|ipad|ipod|ios)/i.test(Hi),Wi=Bi&&Hi.match(/os ([\d_]+)/),zi=Wi&&Wi[1].split("_"),Ui=zi&&Number(zi[0])>=9&&Number(zi[1])>=3&&!window.indexedDB,Ji=void 0,qi=void 0,Qi=void 0,Gi=void 0;if(Ri&&!Mi){var Zi=void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend,Xi=void 0===window.onanimationend&&void 0!==window.onwebkitanimationend;Ji=Zi?"WebkitTransition":"transition",qi=Zi?"webkitTransitionEnd":"transitionend",Qi=Xi?"WebkitAnimation":"animation",Gi=Xi?"webkitAnimationEnd":"animationend"}var Yi=function(){function t(){n=!1;var t=i.slice(0);i=[];for(var e=0;e<t.length;e++)t[e]()}var e,i=[],n=!1;if("undefined"==typeof MutationObserver||Ui){var r=Ri?window:"undefined"!=typeof global?global:{};e=r.setImmediate||setTimeout}else{var s=1,o=new MutationObserver(t),a=document.createTextNode(s);o.observe(a,{characterData:!0}),e=function(){s=(s+1)%2,a.data=s}}return function(r,s){var o=s?function(){r.call(s)}:r;i.push(o),n||(n=!0,e(t,0))}}(),Ki=void 0;"undefined"!=typeof Set&&Set.toString().match(/native code/)?Ki=Set:(Ki=function(){this.set=Object.create(null)},Ki.prototype.has=function(t){return void 0!==this.set[t]},Ki.prototype.add=function(t){this.set[t]=1},Ki.prototype.clear=function(){this.set=Object.create(null)});var tn=$.prototype;tn.put=function(t,e){var i,n=this.get(t,!0);return n||(this.size===this.limit&&(i=this.shift()),n={key:t},this._keymap[t]=n,this.tail?(this.tail.newer=n,n.older=this.tail):this.head=n,this.tail=n,this.size++),n.value=e,i},tn.shift=function(){var t=this.head;return t&&(this.head=this.head.newer,this.head.older=void 0,t.newer=t.older=void 0,this._keymap[t.key]=void 0,this.size--),t},tn.get=function(t,e){var i=this._keymap[t];if(void 0!==i)return i===this.tail?e?i:i.value:(i.newer&&(i===this.head&&(this.head=i.newer),i.newer.older=i.older),i.older&&(i.older.newer=i.newer),i.newer=void 0,i.older=this.tail,this.tail&&(this.tail.newer=i),this.tail=i,e?i:i.value)};var en,nn,rn,sn,on,an,hn,ln,cn,un,fn,pn,dn=new $(1e3),vn=/[^\s'"]+|'[^']*'|"[^"]*"/g,mn=/^in$|^-?\d+/,gn=Object.freeze({parseDirective:A}),_n=/[-.*+?^${}()|[\]\/\\]/g,yn=void 0,bn=void 0,wn=void 0,Cn=/[^|]\|[^|]/,$n=Object.freeze({compileRegex:T,parseText:N,tokensToExp:j}),kn=["{{","}}"],xn=["{{{","}}}"],An=Object.defineProperties({debug:!1,silent:!1,async:!0,warnExpressionErrors:!0,devtools:!1,_delimitersChanged:!0,_assetTypes:["component","directive","elementDirective","filter","transition","partial"],_propBindingModes:{ONE_WAY:0,TWO_WAY:1,ONE_TIME:2},_maxUpdateCount:100},{delimiters:{get:function(){return kn},set:function(t){kn=t,T()},configurable:!0,enumerable:!0},unsafeDelimiters:{get:function(){return xn},set:function(t){xn=t,T()},configurable:!0,enumerable:!0}}),On=void 0,Tn=Object.freeze({appendWithTransition:F,beforeWithTransition:D,removeWithTransition:P,applyTransition:R}),Nn=/^v-ref:/,jn=/^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i,En=/^(slot|partial|component)$/i,Sn=An.optionMergeStrategies=Object.create(null);Sn.data=function(t,e,i){return i?t||e?function(){var n="function"==typeof e?e.call(i):e,r="function"==typeof t?t.call(i):void 0;return n?ut(n,r):r}:void 0:e?"function"!=typeof e?t:t?function(){return ut(e.call(this),t.call(this))}:e:t},Sn.el=function(t,e,i){if(i||!e||"function"==typeof e){var n=e||t;return i&&"function"==typeof n?n.call(i):n}},Sn.init=Sn.created=Sn.ready=Sn.attached=Sn.detached=Sn.beforeCompile=Sn.compiled=Sn.beforeDestroy=Sn.destroyed=Sn.activate=function(t,e){return e?t?t.concat(e):Di(e)?e:[e]:t},An._assetTypes.forEach(function(t){Sn[t+"s"]=ft}),Sn.watch=Sn.events=function(t,e){if(!e)return t;if(!t)return e;var i={};v(i,t);for(var n in e){var r=i[n],s=e[n];r&&!Di(r)&&(r=[r]),i[n]=r?r.concat(s):[s]}return i},Sn.props=Sn.methods=Sn.computed=function(t,e){if(!e)return t;if(!t)return e;var i=Object.create(null);return v(i,t),v(i,e),i};var Fn=function(t,e){return void 0===e?t:e},Dn=0;_t.target=null,_t.prototype.addSub=function(t){this.subs.push(t)},_t.prototype.removeSub=function(t){this.subs.$remove(t)},_t.prototype.depend=function(){_t.target.addDep(this)},_t.prototype.notify=function(){for(var t=d(this.subs),e=0,i=t.length;i>e;e++)t[e].update()};var Pn=Array.prototype,Rn=Object.create(Pn);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(t){var e=Pn[t];_(Rn,t,function(){for(var i=arguments.length,n=new Array(i);i--;)n[i]=arguments[i];var r,s=e.apply(this,n),o=this.__ob__;switch(t){case"push":r=n;break;case"unshift":r=n;break;case"splice":r=n.slice(2)}return r&&o.observeArray(r),o.dep.notify(),s})}),_(Pn,"$set",function(t,e){return t>=this.length&&(this.length=Number(t)+1),this.splice(t,1,e)[0]}),_(Pn,"$remove",function(t){if(this.length){var e=b(this,t);return e>-1?this.splice(e,1):void 0}});var Ln=Object.getOwnPropertyNames(Rn),Hn=!0;bt.prototype.walk=function(t){for(var e=Object.keys(t),i=0,n=e.length;n>i;i++)this.convert(e[i],t[e[i]])},bt.prototype.observeArray=function(t){for(var e=0,i=t.length;i>e;e++)$t(t[e])},bt.prototype.convert=function(t,e){kt(this.value,t,e)},bt.prototype.addVm=function(t){(this.vms||(this.vms=[])).push(t)},bt.prototype.removeVm=function(t){this.vms.$remove(t)};var In=Object.freeze({defineReactive:kt,set:t,del:e,hasOwn:i,isLiteral:n,isReserved:r,_toString:s,toNumber:o,toBoolean:a,stripQuotes:h,camelize:l,hyphenate:u,classify:f,bind:p,toArray:d,extend:v,isObject:m,isPlainObject:g,def:_,debounce:y,indexOf:b,cancellable:w,looseEqual:C,isArray:Di,hasProto:Pi,inBrowser:Ri,devtools:Li,isIE:Ii,isIE9:Mi,isAndroid:Vi,isIos:Bi,iosVersionMatch:Wi,iosVersion:zi,hasMutationObserverBug:Ui,get transitionProp(){return Ji},get transitionEndEvent(){return qi},get animationProp(){return Qi},get animationEndEvent(){return Gi},nextTick:Yi,get _Set(){return Ki},query:L,inDoc:H,getAttr:I,getBindAttr:M,hasBindAttr:V,before:B,after:W,remove:z,prepend:U,replace:J,on:q,off:Q,setClass:Z,addClass:X,removeClass:Y,extractContent:K,trimNode:tt,isTemplate:it,createAnchor:nt,findRef:rt,mapNodeRange:st,removeNodeRange:ot,isFragment:at,getOuterHTML:ht,mergeOptions:mt,resolveAsset:gt,checkComponentAttr:lt,commonTagRE:jn,reservedTagRE:En,warn:On}),Mn=0,Vn=new $(1e3),Bn=0,Wn=1,zn=2,Un=3,Jn=0,qn=1,Qn=2,Gn=3,Zn=4,Xn=5,Yn=6,Kn=7,tr=8,er=[];er[Jn]={ws:[Jn],ident:[Gn,Bn],"[":[Zn],eof:[Kn]},er[qn]={ws:[qn],".":[Qn],"[":[Zn],eof:[Kn]},er[Qn]={ws:[Qn],ident:[Gn,Bn]},er[Gn]={ident:[Gn,Bn],0:[Gn,Bn],number:[Gn,Bn],ws:[qn,Wn],".":[Qn,Wn],"[":[Zn,Wn],eof:[Kn,Wn]},er[Zn]={"'":[Xn,Bn],'"':[Yn,Bn],"[":[Zn,zn],"]":[qn,Un],eof:tr,"else":[Zn,Bn]},er[Xn]={"'":[Zn,Bn],eof:tr,"else":[Xn,Bn]},er[Yn]={'"':[Zn,Bn],eof:tr,"else":[Yn,Bn]};var ir=Object.freeze({parsePath:Nt,getPath:jt,setPath:Et}),nr=new $(1e3),rr="Math,Date,this,true,false,null,undefined,Infinity,NaN,isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,parseInt,parseFloat",sr=new RegExp("^("+rr.replace(/,/g,"\\b|")+"\\b)"),or="break,case,class,catch,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,let,return,super,switch,throw,try,var,while,with,yield,enum,await,implements,package,protected,static,interface,private,public",ar=new RegExp("^("+or.replace(/,/g,"\\b|")+"\\b)"),hr=/\s/g,lr=/\n/g,cr=/[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g,ur=/"(\d+)"/g,fr=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/,pr=/[^\w$\.](?:[A-Za-z_$][\w$]*)/g,dr=/^(?:true|false|null|undefined|Infinity|NaN)$/,vr=[],mr=Object.freeze({parseExpression:It,isSimplePath:Mt}),gr=[],_r=[],yr={},br={},wr=!1,Cr=0;Ut.prototype.get=function(){this.beforeGet();var t,e=this.scope||this.vm;try{t=this.getter.call(e,e)}catch(i){}return this.deep&&Jt(t),this.preProcess&&(t=this.preProcess(t)),this.filters&&(t=e._applyFilters(t,null,this.filters,!1)),this.postProcess&&(t=this.postProcess(t)),this.afterGet(),t},Ut.prototype.set=function(t){var e=this.scope||this.vm;this.filters&&(t=e._applyFilters(t,this.value,this.filters,!0));try{this.setter.call(e,e,t)}catch(i){}var n=e.$forContext;if(n&&n.alias===this.expression){if(n.filters)return;n._withLock(function(){e.$key?n.rawValue[e.$key]=t:n.rawValue.$set(e.$index,t)})}},Ut.prototype.beforeGet=function(){_t.target=this},Ut.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},Ut.prototype.afterGet=function(){_t.target=null;for(var t=this.deps.length;t--;){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var i=this.depIds;this.depIds=this.newDepIds,this.newDepIds=i,this.newDepIds.clear(),i=this.deps,this.deps=this.newDeps,this.newDeps=i,this.newDeps.length=0},Ut.prototype.update=function(t){this.lazy?this.dirty=!0:this.sync||!An.async?this.run():(this.shallow=this.queued?t?this.shallow:!1:!!t,this.queued=!0,zt(this))},Ut.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||(m(t)||this.deep)&&!this.shallow){var e=this.value;this.value=t;this.prevError;this.cb.call(this.vm,t,e)}this.queued=this.shallow=!1}},Ut.prototype.evaluate=function(){var t=_t.target;this.value=this.get(),this.dirty=!1,_t.target=t},Ut.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},Ut.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||this.vm._watchers.$remove(this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1,this.vm=this.cb=this.value=null}};var $r=new Ki,kr={bind:function(){this.attr=3===this.el.nodeType?"data":"textContent"},update:function(t){this.el[this.attr]=s(t)}},xr=new $(1e3),Ar=new $(1e3),Or={efault:[0,"",""],legend:[1,"<fieldset>","</fieldset>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]};Or.td=Or.th=[3,"<table><tbody><tr>","</tr></tbody></table>"],Or.option=Or.optgroup=[1,'<select multiple="multiple">',"</select>"],Or.thead=Or.tbody=Or.colgroup=Or.caption=Or.tfoot=[1,"<table>","</table>"],Or.g=Or.defs=Or.symbol=Or.use=Or.image=Or.text=Or.circle=Or.ellipse=Or.line=Or.path=Or.polygon=Or.polyline=Or.rect=[1,'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"version="1.1">',"</svg>"];var Tr=/<([\w:-]+)/,Nr=/&#?\w+?;/,jr=/<!--/,Er=function(){if(Ri){var t=document.createElement("div");return t.innerHTML="<template>1</template>",!t.cloneNode(!0).firstChild.innerHTML}return!1}(),Sr=function(){if(Ri){var t=document.createElement("textarea");return t.placeholder="t","t"===t.cloneNode(!0).value}return!1}(),Fr=Object.freeze({cloneNode:Zt,parseTemplate:Xt}),Dr={bind:function(){8===this.el.nodeType&&(this.nodes=[],this.anchor=nt("v-html"),J(this.el,this.anchor))},update:function(t){t=s(t),this.nodes?this.swap(t):this.el.innerHTML=t},swap:function(t){for(var e=this.nodes.length;e--;)z(this.nodes[e]);var i=Xt(t,!0,!0);this.nodes=d(i.childNodes),B(i,this.anchor)}};Yt.prototype.callHook=function(t){var e,i;for(e=0,i=this.childFrags.length;i>e;e++)this.childFrags[e].callHook(t);for(e=0,i=this.children.length;i>e;e++)t(this.children[e])},Yt.prototype.beforeRemove=function(){var t,e;for(t=0,e=this.childFrags.length;e>t;t++)this.childFrags[t].beforeRemove(!1);for(t=0,e=this.children.length;e>t;t++)this.children[t].$destroy(!1,!0);var i=this.unlink.dirs;for(t=0,e=i.length;e>t;t++)i[t]._watcher&&i[t]._watcher.teardown()},Yt.prototype.destroy=function(){this.parentFrag&&this.parentFrag.childFrags.$remove(this),this.node.__v_frag=null,this.unlink()};var Pr=new $(5e3);se.prototype.create=function(t,e,i){var n=Zt(this.template);return new Yt(this.linker,this.vm,n,t,e,i)};var Rr=700,Lr=800,Hr=850,Ir=1100,Mr=1500,Vr=1500,Br=1750,Wr=2100,zr=2200,Ur=2300,Jr=0,qr={priority:zr,terminal:!0,params:["track-by","stagger","enter-stagger","leave-stagger"],bind:function(){var t=this.expression.match(/(.*) (?:in|of) (.*)/);if(t){var e=t[1].match(/\((.*),(.*)\)/);e?(this.iterator=e[1].trim(),this.alias=e[2].trim()):this.alias=t[1].trim(),this.expression=t[2]}if(this.alias){this.id="__v-for__"+ ++Jr;var i=this.el.tagName;this.isOption=("OPTION"===i||"OPTGROUP"===i)&&"SELECT"===this.el.parentNode.tagName,this.start=nt("v-for-start"),this.end=nt("v-for-end"),J(this.el,this.end),B(this.start,this.end),this.cache=Object.create(null),this.factory=new se(this.vm,this.el)}},update:function(t){this.diff(t),this.updateRef(),this.updateModel()},diff:function(t){var e,n,r,s,o,a,h=t[0],l=this.fromObject=m(h)&&i(h,"$key")&&i(h,"$value"),c=this.params.trackBy,u=this.frags,f=this.frags=new Array(t.length),p=this.alias,d=this.iterator,v=this.start,g=this.end,_=H(v),y=!u;for(e=0,n=t.length;n>e;e++)h=t[e],s=l?h.$key:null,o=l?h.$value:h,a=!m(o),r=!y&&this.getCachedFrag(o,e,s),r?(r.reused=!0,r.scope.$index=e,s&&(r.scope.$key=s),d&&(r.scope[d]=null!==s?s:e),(c||l||a)&&yt(function(){r.scope[p]=o})):(r=this.create(o,p,e,s),r.fresh=!y),f[e]=r,y&&r.before(g);if(!y){var b=0,w=u.length-f.length;for(this.vm._vForRemoving=!0,e=0,n=u.length;n>e;e++)r=u[e],r.reused||(this.deleteCachedFrag(r),this.remove(r,b++,w,_));this.vm._vForRemoving=!1,b&&(this.vm._watchers=this.vm._watchers.filter(function(t){return t.active}));var C,$,k,x=0;for(e=0,n=f.length;n>e;e++)r=f[e],C=f[e-1],$=C?C.staggerCb?C.staggerAnchor:C.end||C.node:v,r.reused&&!r.staggerCb?(k=oe(r,v,this.id),k===C||k&&oe(k,v,this.id)===C||this.move(r,$)):this.insert(r,x++,$,_),r.reused=r.fresh=!1}},create:function(t,e,i,n){var r=this._host,s=this._scope||this.vm,o=Object.create(s);o.$refs=Object.create(s.$refs),o.$els=Object.create(s.$els),o.$parent=s,o.$forContext=this,yt(function(){kt(o,e,t)}),kt(o,"$index",i),n?kt(o,"$key",n):o.$key&&_(o,"$key",null),this.iterator&&kt(o,this.iterator,null!==n?n:i);var a=this.factory.create(r,o,this._frag);return a.forId=this.id,this.cacheFrag(t,a,i,n),a},updateRef:function(){var t=this.descriptor.ref;if(t){var e,i=(this._scope||this.vm).$refs;this.fromObject?(e={},this.frags.forEach(function(t){e[t.scope.$key]=ae(t)})):e=this.frags.map(ae),i[t]=e}},updateModel:function(){if(this.isOption){var t=this.start.parentNode,e=t&&t.__v_model;e&&e.forceUpdate()}},insert:function(t,e,i,n){t.staggerCb&&(t.staggerCb.cancel(),t.staggerCb=null);var r=this.getStagger(t,e,null,"enter");if(n&&r){var s=t.staggerAnchor;s||(s=t.staggerAnchor=nt("stagger-anchor"),s.__v_frag=t),W(s,i);var o=t.staggerCb=w(function(){t.staggerCb=null,t.before(s),z(s)});setTimeout(o,r)}else{var a=i.nextSibling;a||(W(this.end,i),a=this.end),t.before(a)}},remove:function(t,e,i,n){if(t.staggerCb)return t.staggerCb.cancel(),void(t.staggerCb=null);var r=this.getStagger(t,e,i,"leave");if(n&&r){var s=t.staggerCb=w(function(){t.staggerCb=null,t.remove()});setTimeout(s,r)}else t.remove()},move:function(t,e){e.nextSibling||this.end.parentNode.appendChild(this.end),t.before(e.nextSibling,!1)},cacheFrag:function(t,e,n,r){var s,o=this.params.trackBy,a=this.cache,h=!m(t);r||o||h?(s=le(n,r,t,o),a[s]||(a[s]=e)):(s=this.id,i(t,s)?null===t[s]&&(t[s]=e):Object.isExtensible(t)&&_(t,s,e)),e.raw=t},getCachedFrag:function(t,e,i){var n,r=this.params.trackBy,s=!m(t);if(i||r||s){var o=le(e,i,t,r);n=this.cache[o]}else n=t[this.id];return n&&(n.reused||n.fresh),n},deleteCachedFrag:function(t){var e=t.raw,n=this.params.trackBy,r=t.scope,s=r.$index,o=i(r,"$key")&&r.$key,a=!m(e);if(n||o||a){var h=le(s,o,e,n);this.cache[h]=null}else e[this.id]=null,t.raw=null},getStagger:function(t,e,i,n){n+="Stagger";var r=t.node.__v_trans,s=r&&r.hooks,o=s&&(s[n]||s.stagger);return o?o.call(t,e,i):e*parseInt(this.params[n]||this.params.stagger,10)},_preProcess:function(t){return this.rawValue=t,t},_postProcess:function(t){if(Di(t))return t;if(g(t)){for(var e,i=Object.keys(t),n=i.length,r=new Array(n);n--;)e=i[n],r[n]={$key:e,$value:t[e]};return r}return"number"!=typeof t||isNaN(t)||(t=he(t)),t||[]},unbind:function(){if(this.descriptor.ref&&((this._scope||this.vm).$refs[this.descriptor.ref]=null),this.frags)for(var t,e=this.frags.length;e--;)t=this.frags[e],this.deleteCachedFrag(t),t.destroy()}},Qr={priority:Wr,terminal:!0,bind:function(){var t=this.el;if(t.__vue__)this.invalid=!0;else{var e=t.nextElementSibling;e&&null!==I(e,"v-else")&&(z(e),this.elseEl=e),this.anchor=nt("v-if"),J(t,this.anchor)}},update:function(t){this.invalid||(t?this.frag||this.insert():this.remove())},insert:function(){this.elseFrag&&(this.elseFrag.remove(),this.elseFrag=null),this.factory||(this.factory=new se(this.vm,this.el)),this.frag=this.factory.create(this._host,this._scope,this._frag),this.frag.before(this.anchor)},remove:function(){this.frag&&(this.frag.remove(),this.frag=null),this.elseEl&&!this.elseFrag&&(this.elseFactory||(this.elseFactory=new se(this.elseEl._context||this.vm,this.elseEl)),this.elseFrag=this.elseFactory.create(this._host,this._scope,this._frag),this.elseFrag.before(this.anchor))},unbind:function(){this.frag&&this.frag.destroy(),this.elseFrag&&this.elseFrag.destroy()}},Gr={bind:function(){var t=this.el.nextElementSibling;t&&null!==I(t,"v-else")&&(this.elseEl=t)},update:function(t){this.apply(this.el,t),this.elseEl&&this.apply(this.elseEl,!t)},apply:function(t,e){function i(){t.style.display=e?"":"none"}H(t)?R(t,e?1:-1,i,this.vm):i()}},Zr={bind:function(){var t=this,e=this.el,i="range"===e.type,n=this.params.lazy,r=this.params.number,s=this.params.debounce,a=!1;if(Vi||i||(this.on("compositionstart",function(){a=!0}),this.on("compositionend",function(){a=!1,n||t.listener()})),this.focused=!1,i||n||(this.on("focus",function(){t.focused=!0}),this.on("blur",function(){t.focused=!1,t._frag&&!t._frag.inserted||t.rawListener()})),this.listener=this.rawListener=function(){if(!a&&t._bound){var n=r||i?o(e.value):e.value;t.set(n),Yi(function(){t._bound&&!t.focused&&t.update(t._watcher.value)})}},s&&(this.listener=y(this.listener,s)),this.hasjQuery="function"==typeof jQuery,this.hasjQuery){var h=jQuery.fn.on?"on":"bind";jQuery(e)[h]("change",this.rawListener),n||jQuery(e)[h]("input",this.listener)}else this.on("change",this.rawListener),n||this.on("input",this.listener);!n&&Mi&&(this.on("cut",function(){Yi(t.listener)}),this.on("keyup",function(e){46!==e.keyCode&&8!==e.keyCode||t.listener()})),(e.hasAttribute("value")||"TEXTAREA"===e.tagName&&e.value.trim())&&(this.afterBind=this.listener)},update:function(t){t=s(t),t!==this.el.value&&(this.el.value=t)},unbind:function(){var t=this.el;if(this.hasjQuery){var e=jQuery.fn.off?"off":"unbind";jQuery(t)[e]("change",this.listener),jQuery(t)[e]("input",this.listener)}}},Xr={bind:function(){var t=this,e=this.el;this.getValue=function(){if(e.hasOwnProperty("_value"))return e._value;var i=e.value;return t.params.number&&(i=o(i)),i},this.listener=function(){t.set(t.getValue())},this.on("change",this.listener),e.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){this.el.checked=C(t,this.getValue())}},Yr={bind:function(){var t=this,e=this,i=this.el;this.forceUpdate=function(){e._watcher&&e.update(e._watcher.get())};var n=this.multiple=i.hasAttribute("multiple");this.listener=function(){var t=ce(i,n);t=e.params.number?Di(t)?t.map(o):o(t):t,e.set(t)},this.on("change",this.listener);var r=ce(i,n,!0);(n&&r.length||!n&&null!==r)&&(this.afterBind=this.listener),this.vm.$on("hook:attached",function(){Yi(t.forceUpdate)}),H(i)||Yi(this.forceUpdate)},update:function(t){var e=this.el;e.selectedIndex=-1;for(var i,n,r=this.multiple&&Di(t),s=e.options,o=s.length;o--;)i=s[o],n=i.hasOwnProperty("_value")?i._value:i.value,i.selected=r?ue(t,n)>-1:C(t,n)},unbind:function(){this.vm.$off("hook:attached",this.forceUpdate)}},Kr={bind:function(){function t(){var t=i.checked;return t&&i.hasOwnProperty("_trueValue")?i._trueValue:!t&&i.hasOwnProperty("_falseValue")?i._falseValue:t}var e=this,i=this.el;this.getValue=function(){return i.hasOwnProperty("_value")?i._value:e.params.number?o(i.value):i.value},this.listener=function(){var n=e._watcher.value;if(Di(n)){var r=e.getValue();i.checked?b(n,r)<0&&n.push(r):n.$remove(r)}else e.set(t())},this.on("change",this.listener),i.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){var e=this.el;Di(t)?e.checked=b(t,this.getValue())>-1:e.hasOwnProperty("_trueValue")?e.checked=C(t,e._trueValue):e.checked=!!t}},ts={text:Zr,radio:Xr,select:Yr,checkbox:Kr},es={priority:Lr,twoWay:!0,handlers:ts,params:["lazy","number","debounce"],bind:function(){this.checkFilters(),this.hasRead&&!this.hasWrite;var t,e=this.el,i=e.tagName;if("INPUT"===i)t=ts[e.type]||ts.text;else if("SELECT"===i)t=ts.select;else{if("TEXTAREA"!==i)return;t=ts.text}e.__v_model=this,t.bind.call(this),this.update=t.update,this._unbind=t.unbind},checkFilters:function(){var t=this.filters;if(t)for(var e=t.length;e--;){var i=gt(this.vm.$options,"filters",t[e].name);("function"==typeof i||i.read)&&(this.hasRead=!0),i.write&&(this.hasWrite=!0)}},unbind:function(){this.el.__v_model=null,this._unbind&&this._unbind()}},is={esc:27,tab:9,enter:13,space:32,"delete":[8,46],up:38,left:37,right:39,down:40},ns={priority:Rr,acceptStatement:!0,keyCodes:is,bind:function(){if("IFRAME"===this.el.tagName&&"load"!==this.arg){var t=this;this.iframeBind=function(){q(t.el.contentWindow,t.arg,t.handler,t.modifiers.capture)},this.on("load",this.iframeBind)}},update:function(t){if(this.descriptor.raw||(t=function(){}),"function"==typeof t){this.modifiers.stop&&(t=pe(t)),this.modifiers.prevent&&(t=de(t)),this.modifiers.self&&(t=ve(t));var e=Object.keys(this.modifiers).filter(function(t){return"stop"!==t&&"prevent"!==t&&"self"!==t&&"capture"!==t});e.length&&(t=fe(t,e)),this.reset(),this.handler=t,this.iframeBind?this.iframeBind():q(this.el,this.arg,this.handler,this.modifiers.capture)}},reset:function(){var t=this.iframeBind?this.el.contentWindow:this.el;this.handler&&Q(t,this.arg,this.handler)},unbind:function(){this.reset()}},rs=["-webkit-","-moz-","-ms-"],ss=["Webkit","Moz","ms"],os=/!important;?$/,as=Object.create(null),hs=null,ls={deep:!0,update:function(t){"string"==typeof t?this.el.style.cssText=t:Di(t)?this.handleObject(t.reduce(v,{})):this.handleObject(t||{})},handleObject:function(t){var e,i,n=this.cache||(this.cache={});for(e in n)e in t||(this.handleSingle(e,null),delete n[e]);for(e in t)i=t[e],i!==n[e]&&(n[e]=i,this.handleSingle(e,i))},handleSingle:function(t,e){if(t=me(t))if(null!=e&&(e+=""),e){var i=os.test(e)?"important":"";i?(e=e.replace(os,"").trim(),this.el.style.setProperty(t.kebab,e,i)):this.el.style[t.camel]=e}else this.el.style[t.camel]=""}},cs="http://www.w3.org/1999/xlink",us=/^xlink:/,fs=/^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/,ps=/^(?:value|checked|selected|muted)$/,ds=/^(?:draggable|contenteditable|spellcheck)$/,vs={value:"_value","true-value":"_trueValue","false-value":"_falseValue"},ms={priority:Hr,bind:function(){var t=this.arg,e=this.el.tagName;t||(this.deep=!0);var i=this.descriptor,n=i.interp;n&&(i.hasOneTime&&(this.expression=j(n,this._scope||this.vm)),(fs.test(t)||"name"===t&&("PARTIAL"===e||"SLOT"===e))&&(this.el.removeAttribute(t),this.invalid=!0))},update:function(t){
+if(!this.invalid){var e=this.arg;this.arg?this.handleSingle(e,t):this.handleObject(t||{})}},handleObject:ls.handleObject,handleSingle:function(t,e){var i=this.el,n=this.descriptor.interp;if(this.modifiers.camel&&(t=l(t)),!n&&ps.test(t)&&t in i){var r="value"===t&&null==e?"":e;i[t]!==r&&(i[t]=r)}var s=vs[t];if(!n&&s){i[s]=e;var o=i.__v_model;o&&o.listener()}return"value"===t&&"TEXTAREA"===i.tagName?void i.removeAttribute(t):void(ds.test(t)?i.setAttribute(t,e?"true":"false"):null!=e&&e!==!1?"class"===t?(i.__v_trans&&(e+=" "+i.__v_trans.id+"-transition"),Z(i,e)):us.test(t)?i.setAttributeNS(cs,t,e===!0?"":e):i.setAttribute(t,e===!0?"":e):i.removeAttribute(t))}},gs={priority:Mr,bind:function(){if(this.arg){var t=this.id=l(this.arg),e=(this._scope||this.vm).$els;i(e,t)?e[t]=this.el:kt(e,t,this.el)}},unbind:function(){var t=(this._scope||this.vm).$els;t[this.id]===this.el&&(t[this.id]=null)}},_s={bind:function(){}},ys={bind:function(){var t=this.el;this.vm.$once("pre-hook:compiled",function(){t.removeAttribute("v-cloak")})}},bs={text:kr,html:Dr,"for":qr,"if":Qr,show:Gr,model:es,on:ns,bind:ms,el:gs,ref:_s,cloak:ys},ws={deep:!0,update:function(t){t?"string"==typeof t?this.setClass(t.trim().split(/\s+/)):this.setClass(_e(t)):this.cleanup()},setClass:function(t){this.cleanup(t);for(var e=0,i=t.length;i>e;e++){var n=t[e];n&&ye(this.el,n,X)}this.prevKeys=t},cleanup:function(t){var e=this.prevKeys;if(e)for(var i=e.length;i--;){var n=e[i];(!t||t.indexOf(n)<0)&&ye(this.el,n,Y)}}},Cs={priority:Vr,params:["keep-alive","transition-mode","inline-template"],bind:function(){this.el.__vue__||(this.keepAlive=this.params.keepAlive,this.keepAlive&&(this.cache={}),this.params.inlineTemplate&&(this.inlineTemplate=K(this.el,!0)),this.pendingComponentCb=this.Component=null,this.pendingRemovals=0,this.pendingRemovalCb=null,this.anchor=nt("v-component"),J(this.el,this.anchor),this.el.removeAttribute("is"),this.el.removeAttribute(":is"),this.descriptor.ref&&this.el.removeAttribute("v-ref:"+u(this.descriptor.ref)),this.literal&&this.setComponent(this.expression))},update:function(t){this.literal||this.setComponent(t)},setComponent:function(t,e){if(this.invalidatePending(),t){var i=this;this.resolveComponent(t,function(){i.mountComponent(e)})}else this.unbuild(!0),this.remove(this.childVM,e),this.childVM=null},resolveComponent:function(t,e){var i=this;this.pendingComponentCb=w(function(n){i.ComponentName=n.options.name||("string"==typeof t?t:null),i.Component=n,e()}),this.vm._resolveComponent(t,this.pendingComponentCb)},mountComponent:function(t){this.unbuild(!0);var e=this,i=this.Component.options.activate,n=this.getCached(),r=this.build();i&&!n?(this.waitingFor=r,be(i,r,function(){e.waitingFor===r&&(e.waitingFor=null,e.transition(r,t))})):(n&&r._updateRef(),this.transition(r,t))},invalidatePending:function(){this.pendingComponentCb&&(this.pendingComponentCb.cancel(),this.pendingComponentCb=null)},build:function(t){var e=this.getCached();if(e)return e;if(this.Component){var i={name:this.ComponentName,el:Zt(this.el),template:this.inlineTemplate,parent:this._host||this.vm,_linkerCachable:!this.inlineTemplate,_ref:this.descriptor.ref,_asComponent:!0,_isRouterView:this._isRouterView,_context:this.vm,_scope:this._scope,_frag:this._frag};t&&v(i,t);var n=new this.Component(i);return this.keepAlive&&(this.cache[this.Component.cid]=n),n}},getCached:function(){return this.keepAlive&&this.cache[this.Component.cid]},unbuild:function(t){this.waitingFor&&(this.keepAlive||this.waitingFor.$destroy(),this.waitingFor=null);var e=this.childVM;return!e||this.keepAlive?void(e&&(e._inactive=!0,e._updateRef(!0))):void e.$destroy(!1,t)},remove:function(t,e){var i=this.keepAlive;if(t){this.pendingRemovals++,this.pendingRemovalCb=e;var n=this;t.$remove(function(){n.pendingRemovals--,i||t._cleanup(),!n.pendingRemovals&&n.pendingRemovalCb&&(n.pendingRemovalCb(),n.pendingRemovalCb=null)})}else e&&e()},transition:function(t,e){var i=this,n=this.childVM;switch(n&&(n._inactive=!0),t._inactive=!1,this.childVM=t,i.params.transitionMode){case"in-out":t.$before(i.anchor,function(){i.remove(n,e)});break;case"out-in":i.remove(n,function(){t.$before(i.anchor,e)});break;default:i.remove(n),t.$before(i.anchor,e)}},unbind:function(){if(this.invalidatePending(),this.unbuild(),this.cache){for(var t in this.cache)this.cache[t].$destroy();this.cache=null}}},$s=An._propBindingModes,ks={},xs=/^[$_a-zA-Z]+[\w$]*$/,As=An._propBindingModes,Os={bind:function(){var t=this.vm,e=t._context,i=this.descriptor.prop,n=i.path,r=i.parentPath,s=i.mode===As.TWO_WAY,o=this.parentWatcher=new Ut(e,r,function(e){xe(t,i,e)},{twoWay:s,filters:i.filters,scope:this._scope});if(ke(t,i,o.value),s){var a=this;t.$once("pre-hook:created",function(){a.childWatcher=new Ut(t,n,function(t){o.set(t)},{sync:!0})})}},unbind:function(){this.parentWatcher.teardown(),this.childWatcher&&this.childWatcher.teardown()}},Ts=[],Ns=!1,js="transition",Es="animation",Ss=Ji+"Duration",Fs=Qi+"Duration",Ds=Ri&&window.requestAnimationFrame,Ps=Ds?function(t){Ds(function(){Ds(t)})}:function(t){setTimeout(t,50)},Rs=Se.prototype;Rs.enter=function(t,e){this.cancelPending(),this.callHook("beforeEnter"),this.cb=e,X(this.el,this.enterClass),t(),this.entered=!1,this.callHookWithCb("enter"),this.entered||(this.cancel=this.hooks&&this.hooks.enterCancelled,je(this.enterNextTick))},Rs.enterNextTick=function(){var t=this;this.justEntered=!0,Ps(function(){t.justEntered=!1});var e=this.enterDone,i=this.getCssTransitionType(this.enterClass);this.pendingJsCb?i===js&&Y(this.el,this.enterClass):i===js?(Y(this.el,this.enterClass),this.setupCssCb(qi,e)):i===Es?this.setupCssCb(Gi,e):e()},Rs.enterDone=function(){this.entered=!0,this.cancel=this.pendingJsCb=null,Y(this.el,this.enterClass),this.callHook("afterEnter"),this.cb&&this.cb()},Rs.leave=function(t,e){this.cancelPending(),this.callHook("beforeLeave"),this.op=t,this.cb=e,X(this.el,this.leaveClass),this.left=!1,this.callHookWithCb("leave"),this.left||(this.cancel=this.hooks&&this.hooks.leaveCancelled,this.op&&!this.pendingJsCb&&(this.justEntered?this.leaveDone():je(this.leaveNextTick)))},Rs.leaveNextTick=function(){var t=this.getCssTransitionType(this.leaveClass);if(t){var e=t===js?qi:Gi;this.setupCssCb(e,this.leaveDone)}else this.leaveDone()},Rs.leaveDone=function(){this.left=!0,this.cancel=this.pendingJsCb=null,this.op(),Y(this.el,this.leaveClass),this.callHook("afterLeave"),this.cb&&this.cb(),this.op=null},Rs.cancelPending=function(){this.op=this.cb=null;var t=!1;this.pendingCssCb&&(t=!0,Q(this.el,this.pendingCssEvent,this.pendingCssCb),this.pendingCssEvent=this.pendingCssCb=null),this.pendingJsCb&&(t=!0,this.pendingJsCb.cancel(),this.pendingJsCb=null),t&&(Y(this.el,this.enterClass),Y(this.el,this.leaveClass)),this.cancel&&(this.cancel.call(this.vm,this.el),this.cancel=null)},Rs.callHook=function(t){this.hooks&&this.hooks[t]&&this.hooks[t].call(this.vm,this.el)},Rs.callHookWithCb=function(t){var e=this.hooks&&this.hooks[t];e&&(e.length>1&&(this.pendingJsCb=w(this[t+"Done"])),e.call(this.vm,this.el,this.pendingJsCb))},Rs.getCssTransitionType=function(t){if(!(!qi||document.hidden||this.hooks&&this.hooks.css===!1||Fe(this.el))){var e=this.type||this.typeCache[t];if(e)return e;var i=this.el.style,n=window.getComputedStyle(this.el),r=i[Ss]||n[Ss];if(r&&"0s"!==r)e=js;else{var s=i[Fs]||n[Fs];s&&"0s"!==s&&(e=Es)}return e&&(this.typeCache[t]=e),e}},Rs.setupCssCb=function(t,e){this.pendingCssEvent=t;var i=this,n=this.el,r=this.pendingCssCb=function(s){s.target===n&&(Q(n,t,r),i.pendingCssEvent=i.pendingCssCb=null,!i.pendingJsCb&&e&&e())};q(n,t,r)};var Ls={priority:Ir,update:function(t,e){var i=this.el,n=gt(this.vm.$options,"transitions",t);t=t||"v",e=e||"v",i.__v_trans=new Se(i,t,n,this.vm),Y(i,e+"-transition"),X(i,t+"-transition")}},Hs={style:ls,"class":ws,component:Cs,prop:Os,transition:Ls},Is=/^v-bind:|^:/,Ms=/^v-on:|^@/,Vs=/^v-([^:]+)(?:$|:(.*)$)/,Bs=/\.[^\.]+/g,Ws=/^(v-bind:|:)?transition$/,zs=1e3,Us=2e3;Ye.terminal=!0;var Js=/[^\w\-:\.]/,qs=Object.freeze({compile:De,compileAndLinkProps:Ie,compileRoot:Me,transclude:si,resolveSlots:li}),Qs=/^v-on:|^@/;di.prototype._bind=function(){var t=this.name,e=this.descriptor;if(("cloak"!==t||this.vm._isCompiled)&&this.el&&this.el.removeAttribute){var i=e.attr||"v-"+t;this.el.removeAttribute(i)}var n=e.def;if("function"==typeof n?this.update=n:v(this,n),this._setupParams(),this.bind&&this.bind(),this._bound=!0,this.literal)this.update&&this.update(e.raw);else if((this.expression||this.modifiers)&&(this.update||this.twoWay)&&!this._checkStatement()){var r=this;this.update?this._update=function(t,e){r._locked||r.update(t,e)}:this._update=pi;var s=this._preProcess?p(this._preProcess,this):null,o=this._postProcess?p(this._postProcess,this):null,a=this._watcher=new Ut(this.vm,this.expression,this._update,{filters:this.filters,twoWay:this.twoWay,deep:this.deep,preProcess:s,postProcess:o,scope:this._scope});this.afterBind?this.afterBind():this.update&&this.update(a.value)}},di.prototype._setupParams=function(){if(this.params){var t=this.params;this.params=Object.create(null);for(var e,i,n,r=t.length;r--;)e=u(t[r]),n=l(e),i=M(this.el,e),null!=i?this._setupParamWatcher(n,i):(i=I(this.el,e),null!=i&&(this.params[n]=""===i?!0:i))}},di.prototype._setupParamWatcher=function(t,e){var i=this,n=!1,r=(this._scope||this.vm).$watch(e,function(e,r){if(i.params[t]=e,n){var s=i.paramWatchers&&i.paramWatchers[t];s&&s.call(i,e,r)}else n=!0},{immediate:!0,user:!1});(this._paramUnwatchFns||(this._paramUnwatchFns=[])).push(r)},di.prototype._checkStatement=function(){var t=this.expression;if(t&&this.acceptStatement&&!Mt(t)){var e=It(t).get,i=this._scope||this.vm,n=function(t){i.$event=t,e.call(i,i),i.$event=null};return this.filters&&(n=i._applyFilters(n,null,this.filters)),this.update(n),!0}},di.prototype.set=function(t){this.twoWay&&this._withLock(function(){this._watcher.set(t)})},di.prototype._withLock=function(t){var e=this;e._locked=!0,t.call(e),Yi(function(){e._locked=!1})},di.prototype.on=function(t,e,i){q(this.el,t,e,i),(this._listeners||(this._listeners=[])).push([t,e])},di.prototype._teardown=function(){if(this._bound){this._bound=!1,this.unbind&&this.unbind(),this._watcher&&this._watcher.teardown();var t,e=this._listeners;if(e)for(t=e.length;t--;)Q(this.el,e[t][0],e[t][1]);var i=this._paramUnwatchFns;if(i)for(t=i.length;t--;)i[t]();this.vm=this.el=this._watcher=this._listeners=null}};var Gs=/[^|]\|[^|]/;xt(wi),ui(wi),fi(wi),vi(wi),mi(wi),gi(wi),_i(wi),yi(wi),bi(wi);var Zs={priority:Ur,params:["name"],bind:function(){var t=this.params.name||"default",e=this.vm._slotContents&&this.vm._slotContents[t];e&&e.hasChildNodes()?this.compile(e.cloneNode(!0),this.vm._context,this.vm):this.fallback()},compile:function(t,e,i){if(t&&e){if(this.el.hasChildNodes()&&1===t.childNodes.length&&1===t.childNodes[0].nodeType&&t.childNodes[0].hasAttribute("v-if")){var n=document.createElement("template");n.setAttribute("v-else",""),n.innerHTML=this.el.innerHTML,n._context=this.vm,t.appendChild(n)}var r=i?i._scope:this._scope;this.unlink=e.$compile(t,i,r,this._frag)}t?J(this.el,t):z(this.el)},fallback:function(){this.compile(K(this.el,!0),this.vm)},unbind:function(){this.unlink&&this.unlink()}},Xs={priority:Br,params:["name"],paramWatchers:{name:function(t){Qr.remove.call(this),t&&this.insert(t)}},bind:function(){this.anchor=nt("v-partial"),J(this.el,this.anchor),this.insert(this.params.name)},insert:function(t){var e=gt(this.vm.$options,"partials",t,!0);e&&(this.factory=new se(this.vm,e),Qr.insert.call(this))},unbind:function(){this.frag&&this.frag.destroy()}},Ys={slot:Zs,partial:Xs},Ks=qr._postProcess,to=/(\d{3})(?=\d)/g,eo={orderBy:ki,filterBy:$i,limitBy:Ci,json:{read:function(t,e){return"string"==typeof t?t:JSON.stringify(t,null,arguments.length>1?e:2)},write:function(t){try{return JSON.parse(t)}catch(e){return t}}},capitalize:function(t){return t||0===t?(t=t.toString(),t.charAt(0).toUpperCase()+t.slice(1)):""},uppercase:function(t){return t||0===t?t.toString().toUpperCase():""},lowercase:function(t){return t||0===t?t.toString().toLowerCase():""},currency:function(t,e,i){if(t=parseFloat(t),!isFinite(t)||!t&&0!==t)return"";e=null!=e?e:"$",i=null!=i?i:2;var n=Math.abs(t).toFixed(i),r=i?n.slice(0,-1-i):n,s=r.length%3,o=s>0?r.slice(0,s)+(r.length>3?",":""):"",a=i?n.slice(-1-i):"",h=0>t?"-":"";return h+e+o+r.slice(s).replace(to,"$1,")+a},pluralize:function(t){var e=d(arguments,1),i=e.length;if(i>1){var n=t%10-1;return n in e?e[n]:e[i-1]}return e[0]+(1===t?"":"s")},debounce:function(t,e){return t?(e||(e=300),y(t,e)):void 0}};return Ai(wi),wi.version="1.0.26",setTimeout(function(){An.devtools&&Li&&Li.emit("init",wi)},0),wi});
+//# sourceMappingURL=vue.min.js.map
\ No newline at end of file
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index e5df7b9150e27e777ab823d908906e0adbcd8bc0..935ceef0680bdc5d33b9be9117d60300098a8b51 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -39,3 +39,6 @@ captures/
 
 # Keystore files
 *.jks
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore
index 7a065c709c75460a6cd3cbc49f58b263a6ad1567..8a365b3d82974d95aca80a3b845a7b7b3527e5c4 100644
--- a/vendor/gitignore/C.gitignore
+++ b/vendor/gitignore/C.gitignore
@@ -7,6 +7,11 @@
 *.obj
 *.elf
 
+# Linker output
+*.ilk
+*.map
+*.exp
+
 # Precompiled Headers
 *.gch
 *.pch
@@ -34,3 +39,13 @@
 # Debug files
 *.dSYM/
 *.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+*.mod*
+*.cmd
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf
diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore
index 8e46d5a07f8ffd5032ba920be8a26fe64b650535..3826c85736f8cc3ddbc97449ba3b784a4370428e 100644
--- a/vendor/gitignore/Erlang.gitignore
+++ b/vendor/gitignore/Erlang.gitignore
@@ -4,7 +4,7 @@ deps
 *.beam
 *.plt
 erl_crash.dump
-ebin
+ebin/*.beam
 rel/example_project
 .concrete/DEV_MODE
 .rebar
diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore
index 5ffc21546ec4c944bbac2c25e6566b83dbb5cb76..c92aea0fe0cfa2342e6e13af1b79b73c1930259b 100644
--- a/vendor/gitignore/ExtJs.gitignore
+++ b/vendor/gitignore/ExtJs.gitignore
@@ -1,4 +1,12 @@
 .architect
+bootstrap.css
+bootstrap.js
 bootstrap.json
+bootstrap.jsonp
 build/
+classic.json
+classic.jsonp
 ext/
+modern.json
+modern.jsonp
+resources/sass/.sass-cache/
diff --git a/vendor/gitignore/Global/Ansible.gitignore b/vendor/gitignore/Global/Ansible.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a8b42eb6eed1d00740f6dd332a49c2add9cf6c40
--- /dev/null
+++ b/vendor/gitignore/Global/Ansible.gitignore
@@ -0,0 +1 @@
+*.retry
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index ea83a5eb620684a6c672e78fd2f0fe66d3410fd9..0a254147875173d9bf25903d5e1d837b6cdd3561 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,9 +4,6 @@
 # User-specific stuff:
 .idea/workspace.xml
 .idea/tasks.xml
-.idea/dictionaries
-.idea/vcs.xml
-.idea/jsLibraryMappings.xml
 
 # Sensitive or high-churn files:
 .idea/dataSources.ids
diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore
index cc9586893b6ed581866f0ccbea4584b6abd59a7a..b56bf65d85583b03eeccfaa2a927084583a33e91 100644
--- a/vendor/gitignore/Global/Linux.gitignore
+++ b/vendor/gitignore/Global/Linux.gitignore
@@ -8,3 +8,6 @@
 
 # Linux trash folder which might appear on any partition or disk
 .Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
index 520d91ff584bda8a9385fb02acbbf03e0d35fed3..254108cd23b0df55029e67f56c812f8484ae0e44 100644
--- a/vendor/gitignore/Global/NetBeans.gitignore
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -3,5 +3,4 @@ build/
 nbbuild/
 dist/
 nbdist/
-nbactions.xml
 .nb-gradle/
diff --git a/vendor/gitignore/Global/Tags.gitignore b/vendor/gitignore/Global/Tags.gitignore
index c0318165a2790cedcc8a27765b3e8bc2f586006c..91927af4cd6514b62b66d58a5108b05da96dfcff 100644
--- a/vendor/gitignore/Global/Tags.gitignore
+++ b/vendor/gitignore/Global/Tags.gitignore
@@ -9,6 +9,7 @@ gtags.files
 GTAGS
 GRTAGS
 GPATH
+GSYMS
 cscope.files
 cscope.out
 cscope.in.out
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/macOS.gitignore
similarity index 92%
rename from vendor/gitignore/Global/OSX.gitignore
rename to vendor/gitignore/Global/macOS.gitignore
index 5972fe50f66e4c7b4b5d87afde97758eeeb7c64f..f0f3fbc06c89c36533576e61b30e8cd6ca947b41 100644
--- a/vendor/gitignore/Global/OSX.gitignore
+++ b/vendor/gitignore/Global/macOS.gitignore
@@ -1,25 +1,26 @@
-*.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon

-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-.com.apple.timemachine.donotpresent
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index cd0d5d1e2f4c7647a1bcca8b8927242c3831e6fb..397a0ed4acb63ede454a58674a75984d0ba18308 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -25,3 +25,6 @@ _testmain.go
 
 # Output of the go coverage tool, specifically when used with LiteIDE
 *.out
+
+# external packages folder
+vendor/
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index a4ee41ab62b01d5467e6b817aa1107606ba478d4..450f32ec40cc967988bf7e36f1996ce98e74c0cc 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -17,3 +17,4 @@ cabal.sandbox.config
 *.eventlog
 .stack-work/
 cabal.project.local
+.HTF/
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 0d7a0de298faec7abe9c2592558364fbdda7bde7..93103fdbe772bb02b704a41ab13fe4563685f71e 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -52,6 +52,7 @@
 /administrator/language/en-GB/en-GB.plg_content_contact.sys.ini
 /administrator/language/en-GB/en-GB.plg_content_finder.ini
 /administrator/language/en-GB/en-GB.plg_content_finder.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_module*
 /administrator/language/en-GB/en-GB.plg_finder_categories.ini
 /administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini
 /administrator/language/en-GB/en-GB.plg_finder_contacts.ini
@@ -64,6 +65,10 @@
 /administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini
 /administrator/language/en-GB/en-GB.plg_finder_weblinks.ini
 /administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini
+/administrator/language/en-GB/en-GB.plg_installer_folderinstaller*
+/administrator/language/en-GB/en-GB.plg_installer_packageinstaller*
+/administrator/language/en-GB/en-GB.plg_installer_packageinstaller
+/administrator/language/en-GB/en-GB.plg_installer_urlinstaller*
 /administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini
 /administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini
 /administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini
@@ -72,6 +77,8 @@
 /administrator/language/en-GB/en-GB.plg_search_tags.sys.ini
 /administrator/language/en-GB/en-GB.plg_system_languagecode.ini
 /administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_stats*
+/administrator/language/en-GB/en-GB.plg_system_updatenotification*
 /administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini
 /administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini
 /administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini
@@ -249,8 +256,10 @@
 /administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
 /administrator/language/en-GB/en-GB.xml
 /administrator/language/en-GB/index.html
+/administrator/language/ru-RU/index.html
 /administrator/language/overrides/*
 /administrator/language/index.html
+/administrator/logs/index.html
 /administrator/manifests/*
 /administrator/modules/mod_custom/*
 /administrator/modules/mod_feed/*
@@ -289,6 +298,7 @@
 /components/com_finder/*
 /components/com_mailto/*
 /components/com_media/*
+/components/com_modules/*
 /components/com_newsfeeds/*
 /components/com_search/*
 /components/com_users/*
@@ -407,6 +417,7 @@
 /libraries/idna_convert/*
 /libraries/joomla/*
 /libraries/legacy/*
+/libraries/php-encryption/*
 /libraries/phpass/*
 /libraries/phpmailer/*
 /libraries/phputf8/*
@@ -431,9 +442,11 @@
 /media/media/*
 /media/mod_languages/*
 /media/overrider/*
+/media/plg_captcha_recaptcha/*
 /media/plg_quickicon_extensionupdate/*
 /media/plg_quickicon_joomlaupdate/*
 /media/plg_system_highlight/*
+/media/plg_system_stats/*
 /media/system/*
 /media/index.html
 /modules/mod_articles_archive/*
@@ -486,6 +499,7 @@
 /plugins/editors/none/*
 /plugins/editors/tinymce/*
 /plugins/editors/index.html
+/plugins/editors-xtd/module/*
 /plugins/editors-xtd/article/*
 /plugins/editors-xtd/image/*
 /plugins/editors-xtd/pagebreak/*
@@ -523,6 +537,8 @@
 /plugins/system/redirect/*
 /plugins/system/remember/*
 /plugins/system/sef/*
+/plugins/system/stats/*
+/plugins/system/updatenotification/*
 /plugins/system/index.html
 /plugins/twofactorauth/*
 /plugins/user/contactcreator/*
diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE
index 0e259d42c996742e9e3cba14c677129b2c1b6311..670154e3538863b2d9891fd5483160fbdfc89164 100644
--- a/vendor/gitignore/LICENSE
+++ b/vendor/gitignore/LICENSE
@@ -1,121 +1,116 @@
-Creative Commons Legal Code
-
 CC0 1.0 Universal
 
-    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
-    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
-    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
-    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
-    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
-    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
-    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
-    HEREUNDER.
-
 Statement of Purpose
 
 The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator
-and subsequent owner(s) (each and all, an "owner") of an original work of
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
 authorship and/or a database (each, a "Work").
 
-Certain owners wish to permanently relinquish those rights to a Work for
-the purpose of contributing to a commons of creative, cultural and
-scientific works ("Commons") that the public can reliably and without fear
-of later claims of infringement build upon, modify, incorporate in other
-works, reuse and redistribute as freely as possible in any form whatsoever
-and for any purposes, including without limitation commercial purposes.
-These owners may contribute to the Commons to promote the ideal of a free
-culture and the further production of creative, cultural and scientific
-works, or to gain reputation or greater distribution for their Work in
-part through the use and efforts of others.
-
-For these and/or other purposes and motivations, and without any
-expectation of additional consideration or compensation, the person
-associating CC0 with a Work (the "Affirmer"), to the extent that he or she
-is an owner of Copyright and Related Rights in the Work, voluntarily
-elects to apply CC0 to the Work and publicly distribute the Work under its
-terms, with knowledge of his or her Copyright and Related Rights in the
-Work and the meaning and intended legal effect of CC0 on those rights.
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
 
 1. Copyright and Related Rights. A Work made available under CC0 may be
 protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not
-limited to, the following:
-
-  i. the right to reproduce, adapt, distribute, perform, display,
-     communicate, and translate a Work;
- ii. moral rights retained by the original author(s) and/or performer(s);
-iii. publicity and privacy rights pertaining to a person's image or
-     likeness depicted in a Work;
- iv. rights protecting against unfair competition in regards to a Work,
-     subject to the limitations in paragraph 4(a), below;
-  v. rights protecting the extraction, dissemination, use and reuse of data
-     in a Work;
- vi. database rights (such as those arising under Directive 96/9/EC of the
-     European Parliament and of the Council of 11 March 1996 on the legal
-     protection of databases, and under any national implementation
-     thereof, including any amended or successor version of such
-     directive); and
-vii. other similar, equivalent or corresponding rights throughout the
-     world based on applicable law or treaty, and any national
-     implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention
-of, applicable law, Affirmer hereby overtly, fully, permanently,
-irrevocably and unconditionally waives, abandons, and surrenders all of
-Affirmer's Copyright and Related Rights and associated claims and causes
-of action, whether now known or unknown (including existing as well as
-future claims and causes of action), in the Work (i) in all territories
-worldwide, (ii) for the maximum duration provided by applicable law or
-treaty (including future time extensions), (iii) in any current or future
-medium and for any number of copies, and (iv) for any purpose whatsoever,
-including without limitation commercial, advertising or promotional
-purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
-member of the public at large and to the detriment of Affirmer's heirs and
-successors, fully intending that such Waiver shall not be subject to
-revocation, rescission, cancellation, termination, or any other legal or
-equitable action to disrupt the quiet enjoyment of the Work by the public
-as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason
-be judged legally invalid or ineffective under applicable law, then the
-Waiver shall be preserved to the maximum extent permitted taking into
-account Affirmer's express Statement of Purpose. In addition, to the
-extent the Waiver is so judged Affirmer hereby grants to each affected
-person a royalty-free, non transferable, non sublicensable, non exclusive,
-irrevocable and unconditional license to exercise Affirmer's Copyright and
-Related Rights in the Work (i) in all territories worldwide, (ii) for the
-maximum duration provided by applicable law or treaty (including future
-time extensions), (iii) in any current or future medium and for any number
-of copies, and (iv) for any purpose whatsoever, including without
-limitation commercial, advertising or promotional purposes (the
-"License"). The License shall be deemed effective as of the date CC0 was
-applied by Affirmer to the Work. Should any part of the License for any
-reason be judged legally invalid or ineffective under applicable law, such
-partial invalidity or ineffectiveness shall not invalidate the remainder
-of the License, and in such case Affirmer hereby affirms that he or she
-will not (i) exercise any of his or her remaining Copyright and Related
-Rights in the Work or (ii) assert any associated claims and causes of
-action with respect to the Work, in either case contrary to Affirmer's
-express Statement of Purpose.
+Related Rights"). Copyright and Related Rights include, but are not limited
+to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display, communicate,
+  and translate a Work;
+
+  ii. moral rights retained by the original author(s) and/or performer(s);
+
+  iii. publicity and privacy rights pertaining to a person's image or likeness
+  depicted in a Work;
+
+  iv. rights protecting against unfair competition in regards to a Work,
+  subject to the limitations in paragraph 4(a), below;
+
+  v. rights protecting the extraction, dissemination, use and reuse of data in
+  a Work;
+
+  vi. database rights (such as those arising under Directive 96/9/EC of the
+  European Parliament and of the Council of 11 March 1996 on the legal
+  protection of databases, and under any national implementation thereof,
+  including any amended or successor version of such directive); and
+
+  vii. other similar, equivalent or corresponding rights throughout the world
+  based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time
+extensions), (iii) in any current or future medium and for any number of
+copies, and (iv) for any purpose whatsoever, including without limitation
+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
+the Waiver for the benefit of each member of the public at large and to the
+detriment of Affirmer's heirs and successors, fully intending that such Waiver
+shall not be subject to revocation, rescission, cancellation, termination, or
+any other legal or equitable action to disrupt the quiet enjoyment of the Work
+by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account
+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
+is so judged Affirmer hereby grants to each affected person a royalty-free,
+non transferable, non sublicensable, non exclusive, irrevocable and
+unconditional license to exercise Affirmer's Copyright and Related Rights in
+the Work (i) in all territories worldwide, (ii) for the maximum duration
+provided by applicable law or treaty (including future time extensions), (iii)
+in any current or future medium and for any number of copies, and (iv) for any
+purpose whatsoever, including without limitation commercial, advertising or
+promotional purposes (the "License"). The License shall be deemed effective as
+of the date CC0 was applied by Affirmer to the Work. Should any part of the
+License for any reason be judged legally invalid or ineffective under
+applicable law, such partial invalidity or ineffectiveness shall not
+invalidate the remainder of the License, and in such case Affirmer hereby
+affirms that he or she will not (i) exercise any of his or her remaining
+Copyright and Related Rights in the Work or (ii) assert any associated claims
+and causes of action with respect to the Work, in either case contrary to
+Affirmer's express Statement of Purpose.
 
 4. Limitations and Disclaimers.
 
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
-    surrendered, licensed or otherwise affected by this document.
- b. Affirmer offers the Work as-is and makes no representations or
-    warranties of any kind concerning the Work, express, implied,
-    statutory or otherwise, including without limitation warranties of
-    title, merchantability, fitness for a particular purpose, non
-    infringement, or the absence of latent or other defects, accuracy, or
-    the present or absence of errors, whether or not discoverable, all to
-    the greatest extent permissible under applicable law.
- c. Affirmer disclaims responsibility for clearing rights of other persons
-    that may apply to the Work or any use thereof, including without
-    limitation any person's Copyright and Related Rights in the Work.
-    Further, Affirmer disclaims responsibility for obtaining any necessary
-    consents, permissions or other rights required for any use of the
-    Work.
- d. Affirmer understands and acknowledges that Creative Commons is not a
-    party to this document and has no duty or obligation with respect to
-    this CC0 or use of the Work.
+  a. No trademark or patent rights held by Affirmer are waived, abandoned,
+  surrendered, licensed or otherwise affected by this document.
+
+  b. Affirmer offers the Work as-is and makes no representations or warranties
+  of any kind concerning the Work, express, implied, statutory or otherwise,
+  including without limitation warranties of title, merchantability, fitness
+  for a particular purpose, non infringement, or the absence of latent or
+  other defects, accuracy, or the present or absence of errors, whether or not
+  discoverable, all to the greatest extent permissible under applicable law.
+
+  c. Affirmer disclaims responsibility for clearing rights of other persons
+  that may apply to the Work or any use thereof, including without limitation
+  any person's Copyright and Related Rights in the Work. Further, Affirmer
+  disclaims responsibility for obtaining any necessary consents, permissions
+  or other rights required for any use of the Work.
+
+  d. Affirmer understands and acknowledges that Creative Commons is not a
+  party to this document and has no duty or obligation with respect to this
+  CC0 or use of the Work.
+
+For more information, please see
+<http://creativecommons.org/publicdomain/zero/1.0/>
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index 1cd717b6921f78825776b993f42a451c5fed5a83..e7c594fa3e28d5dc5e1ce08c936b456023c61c09 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -7,6 +7,7 @@ app/storage/
 
 # Laravel 5 & Lumen specific
 bootstrap/cache/
+public/storage
 .env.*.php
 .env.php
 .env
diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore
index abc21828a3ed1662bb28af14afb92b5b7efccb4c..3f36ea2a878791caaaba5a67449af08b774275b3 100644
--- a/vendor/gitignore/Nanoc.gitignore
+++ b/vendor/gitignore/Nanoc.gitignore
@@ -1,6 +1,6 @@
-# For projects using nanoc (http://nanoc.ws/)
+# For projects using Nanoc (http://nanoc.ws/)
 
-# Default location for output, needs to match output_dir's value found in config.yaml
+# Default location for output (needs to match output_dir's value found in nanoc.yaml)
 output/
 
 # Temporary file directory
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index aea5294de9dca6cd0f3edbaab53aab540c7020ca..bc7fc55724c152c385ba6264ca6bf4ebf2e680fb 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -34,5 +34,11 @@ jspm_packages
 # Optional npm cache directory
 .npm
 
+# Optional eslint cache
+.eslintcache
+
 # Optional REPL history
 .node_repl_history
+
+# Output of 'npm pack'
+*.tgz
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 20592083931a5924e8891b6e9916bd3b51b062bd..58c51ecaed4c4d08067f2072b04482ed2cda8819 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -50,7 +50,9 @@ Carthage/Build
 # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
 
 fastlane/report.xml
+fastlane/Preview.html
 fastlane/screenshots
+fastlane/test_output
 
 # Code Injection
 #
diff --git a/vendor/gitignore/OpenCart.gitignore b/vendor/gitignore/OpenCart.gitignore
index 28e45aa6aac4a1f9fffe82ad52e4169bbd1a25f0..97be41faa387f53ec974ea83d42321af7c65e901 100644
--- a/vendor/gitignore/OpenCart.gitignore
+++ b/vendor/gitignore/OpenCart.gitignore
@@ -11,3 +11,10 @@ system/cache/
 system/logs/
 
 system/storage/
+
+# vQmod log files
+vqmod/logs/*
+# vQmod cache files
+vqmod/vqcache/*
+vqmod/checked.cache
+vqmod/mods.cache
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 72364f99fe4bf8d5262df3b19b33102aeaa791e5..6a2bf47ade9c4dd6746a6ceaee6b68c8ce1ce883 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -66,7 +66,7 @@ docs/_build/
 # PyBuilder
 target/
 
-# IPython Notebook
+# Jupyter Notebook
 .ipynb_checkpoints
 
 # pyenv
@@ -79,6 +79,7 @@ celerybeat-schedule
 .env
 
 # virtualenv
+.venv/
 venv/
 ENV/
 
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index d8c256c1925e11e94c5b0a56919ec844517a7329..e97427608c1a0e49deca2c77b170dc433ebd6c8b 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -12,9 +12,11 @@ capybara-*.html
 rerun.txt
 pickle-email-*.html
 
-# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
+# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
 config/initializers/secret_token.rb
-config/secrets.yml
+
+# Only include if you have production secrets in this file, which is no longer a Rails default
+# config/secrets.yml
 
 # dotenv
 # TODO Comment out this rule if environment variables can be committed
diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore
index cb14a420640b9af2da76a8a73650fba0929bfeb8..50281a44270e0fe457f02c7c9edebe234eaa708d 100644
--- a/vendor/gitignore/Rust.gitignore
+++ b/vendor/gitignore/Rust.gitignore
@@ -5,3 +5,6 @@
 # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
 # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
 Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 34f999df3e7e645e84f4dcea7e19ee84cf17cc31..1afbaf197f4c08bf0969488219ce50cb9084bcec 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -61,6 +61,15 @@ acs-*.bib
 # fixme
 *.lox
 
+# feynmf/feynmp
+*.mf
+*.mp
+*.t[1-9]
+*.t[1-9][0-9]
+*.tfm
+*.[1-9]
+*.[1-9][0-9]
+
 #(r)(e)ledmac/(r)(e)ledpar
 *.end
 *.?end
@@ -192,3 +201,6 @@ TSWLatexianTemp*
 
 # KBibTeX
 *~[0-9]*
+
+# auto folder when using emacs and auctex 
+/auto/*
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index be0e4913c3a7194e9aa462dcaf9d0774c5c3216d..beec7b91f15c2f18a5efa6931605f27424f9ee64 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -1,6 +1,9 @@
 # Visual Studio 2015 user specific files
 .vs/
 
+# Visual Studio 2015 database file
+*.VC.db
+
 # Compiled Object files
 *.slo
 *.lo
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 67acbf42f5ee14c6ed7089ef2aa6559f57c860cd..09e407344ca83384d7122ec296e7b2abe42176f4 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -1,11 +1,14 @@
 ## Ignore Visual Studio temporary files, build results, and
 ## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
 
 # User-specific files
 *.suo
 *.user
 *.userosscache
 *.sln.docstates
+*.vcxproj.filters
 
 # User-specific files (MonoDevelop/Xamarin Studio)
 *.userprefs
@@ -44,6 +47,7 @@ dlldata.c
 project.lock.json
 project.fragment.lock.json
 artifacts/
+Properties/launchSettings.json
 
 *_i.c
 *_p.c
@@ -110,6 +114,10 @@ _TeamCity*
 # DotCover is a Code Coverage Tool
 *.dotCover
 
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
 # NCrunch
 _NCrunch_*
 .*crunch*.local.xml
@@ -189,6 +197,7 @@ ClientBin/
 *~
 *.dbmdl
 *.dbproj.schemaview
+*.jfm
 *.pfx
 *.publishsettings
 node_modules/
@@ -233,6 +242,9 @@ FakesAssemblies/
 # Visual Studio 6 workspace options file
 *.opt
 
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
 # Visual Studio LightSwitch build output
 **/*.HTMLClient/GeneratedArtifacts
 **/*.DesktopClient/GeneratedArtifacts
@@ -251,3 +263,13 @@ paket-files/
 # JetBrains Rider
 .idea/
 *.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/
diff --git a/vendor/gitlab-ci-yml/.gitlab-ci.yml b/vendor/gitlab-ci-yml/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..18b14554887ae0ba16b7d45565a35ebcff4e6051
--- /dev/null
+++ b/vendor/gitlab-ci-yml/.gitlab-ci.yml
@@ -0,0 +1,4 @@
+image: ruby:2.3-alpine
+
+test:
+  script: ruby verify_templates.rb
diff --git a/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml b/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f066285b1adf0b2ab48f4789ad2a027a97d35fcc
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml
@@ -0,0 +1,22 @@
+# Based on openjdk:8, already includes lein
+image: clojure:lein-2.7.0
+# If you need to configure a database, add a `services` section here
+# See https://docs.gitlab.com/ce/ci/services/postgres.html
+# Make sure you configure the connection as well
+
+before_script:
+  # If you need to install any external applications, like a 
+  # postgres client, you may want to uncomment the line below:
+  # 
+  #- apt-get update -y
+  #
+  # Retrieve project dependencies
+  # Do this on before_script since it'll be shared between both test and
+  # any production sections a user adds
+  - lein deps
+
+test:
+  script:
+  # If you need to run any migrations or configure the database, this 
+  # would be the point to do it.  
+  - lein test
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8da49a935e3e90c4641004edeb02cfc38c9eb64
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -0,0 +1,37 @@
+# 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"
+
+# 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
+#   - redis:latest
+#   - postgres:latest
+
+# variables:
+#   POSTGRES_DB: database_name
+
+# Cache shards in between builds
+cache:
+  paths:
+    - libs
+
+# This is a basic example for a shard or script which doesn't use
+# services such as redis or postgres
+before_script:
+  - apt-get update -qq && apt-get install -y -qq libxml2-dev
+  - crystal -v # Print out Crystal version for debugging
+  - shards
+
+# If you are using built-in Crystal Spec.
+spec:
+  script:
+  - crystal spec
+
+# If you are using minitest.cr
+minitest:
+  script:
+  - crystal test/spec_test.cr # change to the file(s) you execute for tests
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 396d3f1b042f1c15ea4663c9c43d08f85d0ff9ff..f3fa3949656176b4332b6a5b31fb4c17cb693788 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -1,7 +1,12 @@
 # Official docker image.
 image: docker:latest
 
+services:
+  - docker:dind
+
 build:
   stage: build
   script:
-    - docker build -t test .
+    - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
+    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" .
+    - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME"
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..263c4c199990adfd08cf21daca5253b0f84f3406
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# This template uses the java:8 docker image because there isn't any
+# official Gradle image at this moment
+#
+# This is the Gradle build system for JVM applications
+# https://gradle.org/
+# https://github.com/gradle/gradle
+image: java:8
+
+# 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
+# It is expected that any modern gradle project has a wrapper
+before_script:
+    - chmod +x gradlew
+
+# We redirect the gradle user home using -g so that it caches the
+# wrapper and dependencies.
+# https://docs.gradle.org/current/userguide/gradle_command_line.html
+#
+# Unfortunately it also caches the build output so
+# cleaning removes reminants of any cached builds.
+# The assemble task actually builds the project.
+# If it fails here, the tests can't run.
+build:
+  stage: build
+  script:
+    - ./gradlew -g /cache/.gradle clean assemble
+  allow_failure: false
+
+# Use the generated build output to run the tests.
+test:
+  stage: test
+  script:
+    - ./gradlew -g /cache./gradle check
diff --git a/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..140cb4635f3ad0da70c916839d9d064040be8359
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
@@ -0,0 +1,54 @@
+# An example .gitlab-ci.yml file to test (and optionally report the coverage
+# results of) your [Julia][1] packages. Please refer to the [documentation][2]
+# for more information about package development in Julia.
+#
+# Here, it is assumed that your Julia package is named `MyPackage`. Change it to
+# whatever name you have given to your package.
+#
+# [1]: http://julialang.org/
+# [2]: http://julia.readthedocs.org/
+
+# Below is the template to run your tests in Julia
+.test_template: &test_definition
+  # Uncomment below if you would like to run the tests on specific references
+  # only, such as the branches `master`, `development`, etc.
+  # only:
+  #   - master
+  #   - development
+  script:
+    # Let's run the tests. Substitute `coverage = false` below, if you do not
+    # want coverage results.
+    - /opt/julia/bin/julia -e 'Pkg.clone(pwd()); Pkg.test("MyPackage",
+      coverage = true)'
+    # Comment out below if you do not want coverage results.
+    - /opt/julia/bin/julia -e 'Pkg.add("Coverage"); cd(Pkg.dir("MyPackage"));
+      using Coverage; cl, tl = get_summary(process_folder());
+      println("(", cl/tl*100, "%) covered")'
+
+# Name a test and select an appropriate image.
+test:0.4.6:
+  image: julialang/julia:v0.4.6
+  <<: *test_definition
+
+# Maybe you would like to test your package against the development branch:
+test:0.5.0-dev:
+  image: julialang/julia:v0.5.0-dev
+  # ... allowing for failures, since we are testing against the development
+  # branch:
+  allow_failure: true
+  <<: *test_definition
+
+# REMARK: Do not forget to enable the coverage feature for your project, if you
+# are using code coverage reporting above. This can be done by
+#
+# - Navigating to the `CI/CD Pipelines` settings of your project,
+# - Copying and pasting the default `Simplecov` regex example provided, i.e.,
+#   `\(\d+.\d+\%\) covered` in the `test coverage parsing` textfield.
+#
+# WARNING: This template is using the `julialang/julia` images from [Docker
+# Hub][3]. One can use custom Julia images and/or the official ones found
+# in the same place. However, care must be taken to correctly locate the binary
+# file (`/opt/julia/bin/julia` above), which is usually given on the image's
+# description page.
+#
+# [3]: http://hub.docker.com/
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
index 166f146ee05e1a2c33ade906499ac5dd2de8b0be..08b57c8c0acf4f91c914011a6a477afeffe9f180 100644
--- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -43,3 +43,12 @@ rails:
   - bundle exec rake db:migrate
   - bundle exec rake db:seed
   - bundle exec rake test
+
+# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk
+# are supported too: https://github.com/travis-ci/dpl
+deploy:
+  type: deploy
+  environment: production
+  script:
+  - gem install dpl
+  - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY
diff --git a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c9c35906d1c23bc894a95acb59b0a327f8547c9a
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# Lifted from: https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/
+# This file assumes an own GitLab CI runner, setup on an OS X system.
+stages:
+  - build
+  - archive
+
+build_project:
+  stage: build
+  script:
+    - xcodebuild clean -project ProjectName.xcodeproj -scheme SchemeName | xcpretty
+    - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.2' | xcpretty -s
+  tags:
+    - ios_9-2
+    - xcode_7-2
+    - osx_10-11
+
+archive_project:
+  stage: archive
+  script:
+    - xcodebuild clean archive -archivePath build/ProjectName -scheme SchemeName
+    - xcodebuild -exportArchive -exportFormat ipa -archivePath "build/ProjectName.xcarchive" -exportPath "build/ProjectName.ipa" -exportProvisioningProfile "ProvisioningProfileName"
+  only:
+    - master
+  artifacts:
+    paths:
+    - build/ProjectName.ipa
+  tags:
+    - ios_9-2
+    - xcode_7-2
+    - osx_10-11